Merge commit '1f51f11ab8070005bb33d693e0e94565f57fb88a' into dev-release
diff --git a/build.gradle b/build.gradle
index 921a270..2045d5a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -45,8 +45,8 @@
     mockitoVersion = '2.10.0'
     // The kotlin version is only here to specify the kotlin language level,
     // all kotlin compilations are done in tests.
-    kotlinVersion = '1.5.0'
-    kotlinExtMetadataJVMVersion = '0.3.0'
+    kotlinVersion = '1.6.0'
+    kotlinExtMetadataJVMVersion = '0.4.1'
     smaliVersion = '2.2b4'
     errorproneVersion = '2.3.2'
     testngVersion = '6.10'
@@ -2201,6 +2201,13 @@
         systemProperty 'test_git_HEAD_sha1', project.property('HEAD_sha1')
     }
 
+    if (project.hasProperty('kotlin_compiler_dev')) {
+        systemProperty 'com.android.tools.r8.kotlincompilerdev', '1';
+    }
+
+    if (project.hasProperty('kotlin_compiler_old')) {
+        systemProperty 'com.android.tools.r8.kotlincompilerold', '1';
+    }
 
     if (!useTestingState) {
         testLogging.exceptionFormat = 'full'
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg
index 4623314..31f7a57 100644
--- a/infra/config/global/generated/cr-buildbucket.cfg
+++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -1078,7 +1078,7 @@
         cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build_limited/scripts/slave"
         cipd_version: "refs/heads/master"
         properties_j: "builder_group:\"internal.client.r8\""
-        properties_j: "test_options:[\"--runtimes=dex-default:jdk11\",\"--kotlin-compiler-dev\",\"--one_line_per_test\",\"--archive_failures\",\"--no-internal\",\"*kotlin*\"]"
+        properties_j: "test_options:[\"--runtimes=dex-default:jdk11\",\"--kotlin-compiler-dev\",\"--one_line_per_test\",\"--archive_failures\",\"--no-internal\",\"*kotlin*\",\"*debug*\"]"
       }
       priority: 26
       execution_timeout_secs: 43200
@@ -1107,7 +1107,7 @@
         cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build_limited/scripts/slave"
         cipd_version: "refs/heads/master"
         properties_j: "builder_group:\"internal.client.r8\""
-        properties_j: "test_options:[\"--runtimes=dex-default:jdk11\",\"--kotlin-compiler-old\",\"--one_line_per_test\",\"--archive_failures\",\"--no-internal\",\"*kotlin*\"]"
+        properties_j: "test_options:[\"--runtimes=dex-default:jdk11\",\"--kotlin-compiler-old\",\"--one_line_per_test\",\"--archive_failures\",\"--no-internal\",\"*kotlin*\",\"*debug*\"]"
       }
       priority: 26
       execution_timeout_secs: 43200
diff --git a/infra/config/global/main.star b/infra/config/global/main.star
index 3aa3fe2..6d178eb 100755
--- a/infra/config/global/main.star
+++ b/infra/config/global/main.star
@@ -332,7 +332,7 @@
     expiration_timeout = time.hour * 35,
     properties = {
       "builder_group" : "internal.client.r8",
-      "test_options" : ["--runtimes=dex-default:jdk11", "--kotlin-compiler-dev", "--one_line_per_test", "--archive_failures", "--no-internal", "*kotlin*"]
+      "test_options" : ["--runtimes=dex-default:jdk11", "--kotlin-compiler-dev", "--one_line_per_test", "--archive_failures", "--no-internal", "*kotlin*", "*debug*"]
     }
 )
 
@@ -343,7 +343,7 @@
     expiration_timeout = time.hour * 35,
     properties = {
       "builder_group" : "internal.client.r8",
-      "test_options" : ["--runtimes=dex-default:jdk11", "--kotlin-compiler-old", "--one_line_per_test", "--archive_failures", "--no-internal", "*kotlin*"]
+      "test_options" : ["--runtimes=dex-default:jdk11", "--kotlin-compiler-old", "--one_line_per_test", "--archive_failures", "--no-internal", "*kotlin*", "*debug*"]
     }
 )
 
diff --git a/src/main/java/com/android/tools/r8/DumpOptions.java b/src/main/java/com/android/tools/r8/DumpOptions.java
index e8c6a49..14ae9c6 100644
--- a/src/main/java/com/android/tools/r8/DumpOptions.java
+++ b/src/main/java/com/android/tools/r8/DumpOptions.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.dex.Marker.Tool;
+import com.android.tools.r8.experimental.startup.StartupConfiguration;
 import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryConfiguration;
 import com.android.tools.r8.shaking.ProguardConfiguration;
@@ -147,6 +148,11 @@
     return featureSplitConfiguration;
   }
 
+  public StartupConfiguration getStartupConfiguration() {
+    // The startup configuration is not included in dumps.
+    return null;
+  }
+
   public String getParsedProguardConfiguration() {
     return proguardConfiguration == null ? null : proguardConfiguration.getParsedConfiguration();
   }
diff --git a/src/main/java/com/android/tools/r8/FeatureSplit.java b/src/main/java/com/android/tools/r8/FeatureSplit.java
index d0eafdb..9b55bde 100644
--- a/src/main/java/com/android/tools/r8/FeatureSplit.java
+++ b/src/main/java/com/android/tools/r8/FeatureSplit.java
@@ -38,6 +38,14 @@
         }
       };
 
+  public static final FeatureSplit BASE_STARTUP =
+      new FeatureSplit(null, null) {
+        @Override
+        public boolean isBase() {
+          return true;
+        }
+      };
+
   private final ProgramConsumer programConsumer;
   private final List<ProgramResourceProvider> programResourceProviders;
 
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java
index 9946047..b0eec1a 100644
--- a/src/main/java/com/android/tools/r8/L8.java
+++ b/src/main/java/com/android/tools/r8/L8.java
@@ -19,7 +19,6 @@
 import com.android.tools.r8.naming.PrefixRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
 import com.android.tools.r8.origin.CommandLineOrigin;
-import com.android.tools.r8.shaking.AnnotationRemover;
 import com.android.tools.r8.shaking.L8TreePruner;
 import com.android.tools.r8.synthesis.SyntheticFinalization;
 import com.android.tools.r8.utils.AndroidApp;
@@ -135,10 +134,6 @@
 
       AppView<AppInfo> appView = readApp(inputApp, options, executor, timing);
 
-      if (!options.testing.disableL8AnnotationRemoval) {
-        AnnotationRemover.clearAnnotations(appView);
-      }
-
       new IRConverter(appView, timing).convert(appView, executor);
 
       SyntheticFinalization.finalize(appView, executor);
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index cec2d83..4b53393 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
+import com.android.tools.r8.experimental.startup.StartupConfiguration;
 import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.inspector.Inspector;
@@ -819,6 +820,10 @@
     skipDump = false;
   }
 
+  public DexItemFactory getDexItemFactory() {
+    return proguardConfiguration.getDexItemFactory();
+  }
+
   /** Get the enable-tree-shaking state. */
   public boolean getEnableTreeShaking() {
     return enableTreeShaking;
@@ -905,6 +910,9 @@
 
     internal.featureSplitConfiguration = featureSplitConfiguration;
 
+    internal.startupConfiguration =
+        StartupConfiguration.createStartupConfiguration(getDexItemFactory(), getReporter());
+
     internal.syntheticProguardRulesConsumer = syntheticProguardRulesConsumer;
 
     internal.outputInspections = InspectorImpl.wrapInspections(getOutputInspections());
diff --git a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
index e402ce5..990d545 100644
--- a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
+++ b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.dex;
 
 import static com.android.tools.r8.graph.DexCode.TryHandler.NO_HANDLER;
+import static com.android.tools.r8.graph.DexDebugEventBuilder.addDefaultEventWithAdvancePcIfNecessary;
 
 import com.android.tools.r8.code.ConstString;
 import com.android.tools.r8.code.ConstStringJumbo;
@@ -233,7 +234,7 @@
           Instruction target = debugEventTargets.get(lastOriginalOffset);
           int lineDelta = defaultEvent.getLineDelta();
           int pcDelta = target.getOffset() - lastNewOffset;
-          addDefaultEvent(lineDelta, pcDelta, events);
+          addDefaultEventWithAdvancePcIfNecessary(lineDelta, pcDelta, events, factory);
           lastNewOffset = target.getOffset();
         } else {
           events.add(event);
@@ -247,27 +248,6 @@
     return code.getDebugInfo();
   }
 
-  // Add a default event. If the lineDelta and pcDelta can be encoded in one default event
-  // that will be done. Otherwise, this can output an advance line and/or advance pc event
-  // followed by a default event. A default event is always emitted as that is what will
-  // materialize an entry in the line table.
-  private void addDefaultEvent(int lineDelta, int pcDelta, List<DexDebugEvent> events) {
-    if (lineDelta < Constants.DBG_LINE_BASE
-        || lineDelta - Constants.DBG_LINE_BASE >= Constants.DBG_LINE_RANGE) {
-      events.add(factory.createAdvanceLine(lineDelta));
-      lineDelta = 0;
-    }
-    if (pcDelta >= Constants.DBG_ADDRESS_RANGE) {
-      events.add(factory.createAdvancePC(pcDelta));
-      pcDelta = 0;
-    }
-    int specialOpcode =
-        0x0a + (lineDelta - Constants.DBG_LINE_BASE) + Constants.DBG_LINE_RANGE * pcDelta;
-    assert specialOpcode >= 0x0a;
-    assert specialOpcode <= 0xff;
-    events.add(factory.createDefault(specialOpcode));
-  }
-
   private List<Instruction> expandCode() {
     LinkedList<Instruction> instructions = new LinkedList<>();
     Collections.addAll(instructions, method.getCode().asDexCode().instructions);
diff --git a/src/main/java/com/android/tools/r8/experimental/startup/StartupConfiguration.java b/src/main/java/com/android/tools/r8/experimental/startup/StartupConfiguration.java
new file mode 100644
index 0000000..c87d584
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/startup/StartupConfiguration.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.experimental.startup;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringDiagnostic;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+public class StartupConfiguration {
+
+  List<DexType> startupClasses;
+
+  public StartupConfiguration(List<DexType> startupClasses) {
+    this.startupClasses = startupClasses;
+  }
+
+  public static StartupConfiguration createStartupConfiguration(
+      DexItemFactory dexItemFactory, Reporter reporter) {
+    String propertyValue = System.getProperty("com.android.tools.r8.startupclassdescriptors");
+    if (propertyValue == null) {
+      return null;
+    }
+
+    List<String> startupClassDescriptors;
+    try {
+      startupClassDescriptors = FileUtils.readAllLines(Paths.get(propertyValue));
+    } catch (IOException e) {
+      throw reporter.fatalError(new ExceptionDiagnostic(e));
+    }
+
+    if (startupClassDescriptors.isEmpty()) {
+      return null;
+    }
+
+    List<DexType> startupClasses = new ArrayList<>(startupClassDescriptors.size());
+    for (String startupClassDescriptor : startupClassDescriptors) {
+      if (startupClassDescriptor.trim().isEmpty()) {
+        continue;
+      }
+      if (!DescriptorUtils.isClassDescriptor(startupClassDescriptor)) {
+        reporter.warning(
+            new StringDiagnostic(
+                "Invalid class descriptor for startup class: " + startupClassDescriptor));
+        continue;
+      }
+      DexType startupClass = dexItemFactory.createType(startupClassDescriptor);
+      startupClasses.add(startupClass);
+    }
+    return new StartupConfiguration(startupClasses);
+  }
+
+  public boolean hasStartupClasses() {
+    return !startupClasses.isEmpty();
+  }
+
+  public List<DexType> getStartupClasses() {
+    return startupClasses;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
index f460b50..215b25a 100644
--- a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
+++ b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
@@ -8,7 +8,7 @@
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResourceProvider;
 import com.android.tools.r8.ResourceException;
-import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.experimental.startup.StartupConfiguration;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -22,78 +22,96 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.Sets;
-import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
 
 public class ClassToFeatureSplitMap {
 
-  private final Map<DexType, FeatureSplit> classToFeatureSplitMap = new IdentityHashMap<>();
+  private final FeatureSplit baseStartup;
+  private final Map<DexType, FeatureSplit> classToFeatureSplitMap;
   private final Map<FeatureSplit, String> representativeStringsForFeatureSplit;
 
-  private ClassToFeatureSplitMap() {
-    this(new HashMap<>());
-  }
-
-  private ClassToFeatureSplitMap(Map<FeatureSplit, String> representativeStringsForFeatureSplit) {
+  private ClassToFeatureSplitMap(
+      FeatureSplit baseStartup,
+      Map<DexType, FeatureSplit> classToFeatureSplitMap,
+      Map<FeatureSplit, String> representativeStringsForFeatureSplit) {
+    this.baseStartup = baseStartup;
+    this.classToFeatureSplitMap = classToFeatureSplitMap;
     this.representativeStringsForFeatureSplit = representativeStringsForFeatureSplit;
   }
 
   public static ClassToFeatureSplitMap createEmptyClassToFeatureSplitMap() {
-    return new ClassToFeatureSplitMap(null);
-  }
-
-  public static ClassToFeatureSplitMap createInitialClassToFeatureSplitMap(
-      AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return createInitialClassToFeatureSplitMap(appView.options());
+    return new ClassToFeatureSplitMap(FeatureSplit.BASE, new IdentityHashMap<>(), null);
   }
 
   public static ClassToFeatureSplitMap createInitialClassToFeatureSplitMap(
       InternalOptions options) {
     return createInitialClassToFeatureSplitMap(
-        options.dexItemFactory(), options.featureSplitConfiguration, options.reporter);
+        options.dexItemFactory(),
+        options.featureSplitConfiguration,
+        options.startupConfiguration,
+        options.reporter);
   }
 
   public static ClassToFeatureSplitMap createInitialClassToFeatureSplitMap(
       DexItemFactory dexItemFactory,
       FeatureSplitConfiguration featureSplitConfiguration,
+      StartupConfiguration startupConfiguration,
       Reporter reporter) {
-
-    ClassToFeatureSplitMap result = new ClassToFeatureSplitMap();
-    if (featureSplitConfiguration == null) {
-      return result;
+    if (featureSplitConfiguration == null && startupConfiguration == null) {
+      return createEmptyClassToFeatureSplitMap();
     }
 
-    for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
-      String representativeType = null;
-      for (ProgramResourceProvider programResourceProvider :
-          featureSplit.getProgramResourceProviders()) {
-        try {
-          for (ProgramResource programResource : programResourceProvider.getProgramResources()) {
-            for (String classDescriptor : programResource.getClassDescriptors()) {
-              DexType type = dexItemFactory.createType(classDescriptor);
-              result.classToFeatureSplitMap.put(type, featureSplit);
-              if (representativeType == null || classDescriptor.compareTo(representativeType) > 0) {
-                representativeType = classDescriptor;
+    Map<DexType, FeatureSplit> classToFeatureSplitMap = new IdentityHashMap<>();
+    Map<FeatureSplit, String> representativeStringsForFeatureSplit = new IdentityHashMap<>();
+    if (featureSplitConfiguration != null) {
+      for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
+        String representativeType = null;
+        for (ProgramResourceProvider programResourceProvider :
+            featureSplit.getProgramResourceProviders()) {
+          try {
+            for (ProgramResource programResource : programResourceProvider.getProgramResources()) {
+              for (String classDescriptor : programResource.getClassDescriptors()) {
+                DexType type = dexItemFactory.createType(classDescriptor);
+                classToFeatureSplitMap.put(type, featureSplit);
+                if (representativeType == null
+                    || classDescriptor.compareTo(representativeType) > 0) {
+                  representativeType = classDescriptor;
+                }
               }
             }
+          } catch (ResourceException e) {
+            throw reporter.fatalError(e.getMessage());
           }
-        } catch (ResourceException e) {
-          throw reporter.fatalError(e.getMessage());
+        }
+        if (representativeType != null) {
+          representativeStringsForFeatureSplit.put(featureSplit, representativeType);
         }
       }
-      if (representativeType != null) {
-        result.representativeStringsForFeatureSplit.put(featureSplit, representativeType);
-      }
     }
-    return result;
-  }
 
-  public int compareFeatureSplitsForDexTypes(DexType a, DexType b, SyntheticItems syntheticItems) {
-    FeatureSplit featureSplitA = getFeatureSplit(a, syntheticItems);
-    FeatureSplit featureSplitB = getFeatureSplit(b, syntheticItems);
-    return compareFeatureSplits(featureSplitA, featureSplitB);
+    FeatureSplit baseStartup;
+    if (startupConfiguration != null && startupConfiguration.hasStartupClasses()) {
+      DexType representativeType = null;
+      for (DexType startupClass : startupConfiguration.getStartupClasses()) {
+        if (classToFeatureSplitMap.containsKey(startupClass)) {
+          continue;
+        }
+        classToFeatureSplitMap.put(startupClass, FeatureSplit.BASE_STARTUP);
+        if (representativeType == null
+            || startupClass.getDescriptor().compareTo(representativeType.getDescriptor()) > 0) {
+          representativeType = startupClass;
+        }
+      }
+      baseStartup = FeatureSplit.BASE_STARTUP;
+      representativeStringsForFeatureSplit.put(
+          baseStartup, representativeType.toDescriptorString());
+    } else {
+      baseStartup = FeatureSplit.BASE;
+    }
+    return new ClassToFeatureSplitMap(
+        baseStartup, classToFeatureSplitMap, representativeStringsForFeatureSplit);
   }
 
   public int compareFeatureSplits(FeatureSplit featureSplitA, FeatureSplit featureSplitB) {
@@ -114,6 +132,14 @@
         .compareTo(representativeStringsForFeatureSplit.get(featureSplitB));
   }
 
+  /**
+   * Returns the base startup if there are any startup classes given on input. Otherwise returns
+   * base.
+   */
+  public FeatureSplit getBaseStartup() {
+    return baseStartup;
+  }
+
   public Map<FeatureSplit, Set<DexProgramClass>> getFeatureSplitClasses(
       Set<DexProgramClass> classes, SyntheticItems syntheticItems) {
     Map<FeatureSplit, Set<DexProgramClass>> result = new IdentityHashMap<>();
@@ -135,7 +161,7 @@
     if (feature != null) {
       return feature;
     }
-    feature = syntheticItems.getContextualFeatureSplit(type);
+    feature = syntheticItems.getContextualFeatureSplit(type, this);
     if (feature != null) {
       return feature;
     }
@@ -168,23 +194,18 @@
     return !isInBase(clazz, syntheticItems);
   }
 
-  public boolean isInSameFeatureOrBothInBase(
+  public boolean isInSameFeatureOrBothInSameBase(
       ProgramMethod a, ProgramMethod b, SyntheticItems syntheticItems) {
-    return isInSameFeatureOrBothInBase(a.getHolder(), b.getHolder(), syntheticItems);
+    return isInSameFeatureOrBothInSameBase(a.getHolder(), b.getHolder(), syntheticItems);
   }
 
-  public boolean isInSameFeatureOrBothInBase(
+  public boolean isInSameFeatureOrBothInSameBase(
       DexProgramClass a, DexProgramClass b, SyntheticItems syntheticItems) {
     return getFeatureSplit(a, syntheticItems) == getFeatureSplit(b, syntheticItems);
   }
 
-  public boolean isInSameFeatureOrBothInBase(DexType a, DexType b, SyntheticItems syntheticItems) {
-    return getFeatureSplit(a, syntheticItems) == getFeatureSplit(b, syntheticItems);
-  }
-
   public ClassToFeatureSplitMap rewrittenWithLens(GraphLens lens) {
-    ClassToFeatureSplitMap rewrittenClassToFeatureSplitMap =
-        new ClassToFeatureSplitMap(representativeStringsForFeatureSplit);
+    Map<DexType, FeatureSplit> rewrittenClassToFeatureSplitMap = new IdentityHashMap<>();
     classToFeatureSplitMap.forEach(
         (type, featureSplit) -> {
           DexType rewrittenType = lens.lookupType(type);
@@ -192,25 +213,24 @@
             // The type was removed by enum unboxing.
             return;
           }
-          FeatureSplit existing =
-              rewrittenClassToFeatureSplitMap.classToFeatureSplitMap.put(
-                  rewrittenType, featureSplit);
+          FeatureSplit existing = rewrittenClassToFeatureSplitMap.put(rewrittenType, featureSplit);
           // If we map two classes to the same class then they must be from the same feature split.
           assert existing == null || existing == featureSplit;
         });
-    return rewrittenClassToFeatureSplitMap;
+    return new ClassToFeatureSplitMap(
+        baseStartup, rewrittenClassToFeatureSplitMap, representativeStringsForFeatureSplit);
   }
 
   public ClassToFeatureSplitMap withoutPrunedItems(PrunedItems prunedItems) {
-    ClassToFeatureSplitMap classToFeatureSplitMapAfterPruning =
-        new ClassToFeatureSplitMap(representativeStringsForFeatureSplit);
+    Map<DexType, FeatureSplit> rewrittenClassToFeatureSplitMap = new IdentityHashMap<>();
     classToFeatureSplitMap.forEach(
         (type, featureSplit) -> {
           if (!prunedItems.getRemovedClasses().contains(type)) {
-            classToFeatureSplitMapAfterPruning.classToFeatureSplitMap.put(type, featureSplit);
+            rewrittenClassToFeatureSplitMap.put(type, featureSplit);
           }
         });
-    return classToFeatureSplitMapAfterPruning;
+    return new ClassToFeatureSplitMap(
+        baseStartup, rewrittenClassToFeatureSplitMap, representativeStringsForFeatureSplit);
   }
 
   // Static helpers to avoid verbose predicates.
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index e5a744b..1cf6ae7 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
-import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
 import com.android.tools.r8.origin.Origin;
@@ -13,7 +12,6 @@
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.IterableUtils;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -125,26 +123,6 @@
     return syntheticItems;
   }
 
-  public void addSynthesizedClassForLibraryDesugaring(DexProgramClass clazz) {
-    assert checkIfObsolete();
-    assert options().desugaredLibraryConfiguration != null;
-    syntheticItems.addLegacySyntheticClassForLibraryDesugaring(clazz);
-  }
-
-  public void addSynthesizedClass(DexProgramClass clazz, ProgramDefinition context) {
-    assert checkIfObsolete();
-    assert context != null;
-    syntheticItems.addLegacySyntheticClass(clazz, context, FeatureSplit.BASE);
-  }
-
-  public void addSynthesizedClassToBase(DexProgramClass clazz, Iterable<DexProgramClass> contexts) {
-    assert checkIfObsolete();
-    assert !IterableUtils.isEmpty(contexts);
-    SyntheticItems syntheticItems = getSyntheticItems();
-    contexts.forEach(
-        context -> syntheticItems.addLegacySyntheticClass(clazz, context, FeatureSplit.BASE));
-  }
-
   public List<DexProgramClass> classes() {
     assert checkIfObsolete();
     return app.classes();
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index 4eaea9f..c928847 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.TraversalContinuation.BREAK;
 import static com.android.tools.r8.utils.TraversalContinuation.CONTINUE;
 
-import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.MethodResolutionResult.ArrayCloneMethodResult;
@@ -161,15 +160,6 @@
     return this;
   }
 
-  @Override
-  public void addSynthesizedClass(DexProgramClass clazz, ProgramDefinition context) {
-    assert checkIfObsolete();
-    assert context != null;
-    FeatureSplit featureSplit =
-        classToFeatureSplitMap.getFeatureSplit(context, getSyntheticItems());
-    getSyntheticItems().addLegacySyntheticClass(clazz, context, featureSplit);
-  }
-
   /** Primitive traversal over all (non-interface) superclasses of a given type. */
   public TraversalContinuation traverseSuperClasses(
       DexClass clazz, TriFunction<DexType, DexClass, DexClass, TraversalContinuation> fn) {
diff --git a/src/main/java/com/android/tools/r8/graph/AppServices.java b/src/main/java/com/android/tools/r8/graph/AppServices.java
index 7fedd7c..b5b0c60 100644
--- a/src/main/java/com/android/tools/r8/graph/AppServices.java
+++ b/src/main/java/com/android/tools/r8/graph/AppServices.java
@@ -103,21 +103,28 @@
               + "`";
       return true;
     }
-    if (featureImplementations.size() > 1
-        || !featureImplementations.containsKey(FeatureSplit.BASE)) {
+    if (featureImplementations.keySet().stream().anyMatch(feature -> !feature.isBase())) {
       return true;
     }
+    // All service implementations are in one of the base splits.
+    assert featureImplementations.size() <= 2;
     // Check if service is defined feature
     DexProgramClass serviceClass = appView.definitionForProgramType(serviceType);
     if (serviceClass != null
         && classToFeatureSplitMap.isInFeature(serviceClass, appView.getSyntheticItems())) {
       return true;
     }
-    for (DexType implementationType : featureImplementations.get(FeatureSplit.BASE)) {
-      DexProgramClass implementationClass = appView.definitionForProgramType(implementationType);
-      if (implementationClass != null
-          && classToFeatureSplitMap.isInFeature(implementationClass, appView.getSyntheticItems())) {
-        return true;
+    for (Entry<FeatureSplit, List<DexType>> entry : featureImplementations.entrySet()) {
+      FeatureSplit feature = entry.getKey();
+      assert feature.isBase();
+      List<DexType> implementationTypes = entry.getValue();
+      for (DexType implementationType : implementationTypes) {
+        DexProgramClass implementationClass = appView.definitionForProgramType(implementationType);
+        if (implementationClass != null
+            && classToFeatureSplitMap.isInFeature(
+                implementationClass, appView.getSyntheticItems())) {
+          return true;
+        }
       }
     }
     return false;
@@ -216,6 +223,7 @@
 
     public AppServices build() {
       for (DataResourceProvider provider : appView.appInfo().app().dataResourceProviders) {
+        // TODO(b/208677025): This should use BASE_STARTUP for startup classes.
         readServices(provider, FeatureSplit.BASE);
       }
       if (options.featureSplitConfiguration != null) {
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 e21fbf4..303eaf8e 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepClassInfo;
+import com.android.tools.r8.shaking.KeepFieldInfo;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.shaking.KeepMethodInfo;
 import com.android.tools.r8.shaking.LibraryModeledPredicate;
@@ -213,6 +214,10 @@
     return simpleInliningConstraintFactory;
   }
 
+  public DexApplication app() {
+    return appInfo().app();
+  }
+
   public T appInfo() {
     assert !appInfo.hasClassHierarchy() || enableWholeProgramOptimizations();
     return appInfo;
@@ -533,6 +538,10 @@
     return getKeepInfo().getClassInfo(clazz);
   }
 
+  public KeepFieldInfo getKeepInfo(ProgramField field) {
+    return getKeepInfo().getFieldInfo(field);
+  }
+
   public KeepMethodInfo getKeepInfo(ProgramMethod method) {
     return getKeepInfo().getMethodInfo(method);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index 309a768..d0a336a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -22,7 +22,7 @@
 import java.util.List;
 import java.util.function.Predicate;
 
-public abstract class DexApplication {
+public abstract class DexApplication implements DexDefinitionSupplier {
 
   public final ImmutableList<DataResourceProvider> dataResourceProviders;
 
@@ -56,6 +56,11 @@
 
   public abstract Builder<?> builder();
 
+  @Override
+  public DexItemFactory dexItemFactory() {
+    return dexItemFactory;
+  }
+
   public DexDefinitionSupplier getDefinitionsSupplier(
       SyntheticDefinitionsProvider syntheticDefinitionsProvider) {
     DexApplication self = this;
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 e359150..6cef0f6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -318,7 +318,7 @@
   }
 
   public void forEachFieldMatching(
-      Predicate<DexEncodedField> predicate, Consumer<DexEncodedField> consumer) {
+      Predicate<? super DexEncodedField> predicate, Consumer<? super DexEncodedField> consumer) {
     fields(predicate).forEach(consumer);
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
index 8091db7..71822f6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
@@ -261,6 +261,11 @@
           factory.createSetOutlineCallerFrame(
               nextPosition.getOutlineCallee(), nextPosition.getOutlinePositions()));
     }
+    addDefaultEventWithAdvancePcIfNecessary(lineDelta, pcDelta, events, factory);
+  }
+
+  public static void addDefaultEventWithAdvancePcIfNecessary(
+      int lineDelta, int pcDelta, List<DexDebugEvent> events, DexItemFactory factory) {
     if (lineDelta < Constants.DBG_LINE_BASE
         || lineDelta - Constants.DBG_LINE_BASE >= Constants.DBG_LINE_RANGE) {
       events.add(factory.createAdvanceLine(lineDelta));
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 2bbb51b..2e09eec 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -196,7 +196,12 @@
   }
 
   public void forEachProgramField(Consumer<? super ProgramField> consumer) {
-    forEachField(field -> consumer.accept(new ProgramField(this, field)));
+    forEachProgramFieldMatching(alwaysTrue(), consumer);
+  }
+
+  public void forEachProgramFieldMatching(
+      Predicate<? super DexEncodedField> predicate, Consumer<? super ProgramField> consumer) {
+    forEachFieldMatching(predicate, field -> consumer.accept(new ProgramField(this, field)));
   }
 
   public void forEachProgramInstanceField(Consumer<? super ProgramField> consumer) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index b3e8f74..8315b20 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -7,6 +7,8 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.Unreachable;
+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.references.ClassReference;
@@ -54,8 +56,21 @@
     return Reference.classFromDescriptor(toDescriptorString());
   }
 
+  public DynamicTypeWithUpperBound toDynamicType(AppView<AppInfoWithLiveness> appView) {
+    return toDynamicType(appView, Nullability.maybeNull());
+  }
+
+  public DynamicTypeWithUpperBound toDynamicType(
+      AppView<AppInfoWithLiveness> appView, Nullability nullability) {
+    return DynamicType.create(appView, toTypeElement(appView, nullability));
+  }
+
   public TypeElement toTypeElement(AppView<?> appView) {
-    return TypeElement.fromDexType(this, Nullability.maybeNull(), appView);
+    return toTypeElement(appView, Nullability.maybeNull());
+  }
+
+  public TypeElement toTypeElement(AppView<?> appView, Nullability nullability) {
+    return TypeElement.fromDexType(this, nullability, appView);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
index 5328885..0a60b01 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.IteratorUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Ordering;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry;
 import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap;
@@ -49,6 +50,12 @@
           }
 
           @Override
+          public ArgumentInfo rewrittenWithLens(
+              AppView<AppInfoWithLiveness> appView, GraphLens graphLens) {
+            return this;
+          }
+
+          @Override
           public boolean equals(Object obj) {
             return obj == this;
           }
@@ -91,6 +98,9 @@
     // ArgumentInfo are combined with `this` first, and the `info` argument second.
     public abstract ArgumentInfo combine(ArgumentInfo info);
 
+    public abstract ArgumentInfo rewrittenWithLens(
+        AppView<AppInfoWithLiveness> appView, GraphLens graphLens);
+
     @Override
     public abstract boolean equals(Object obj);
 
@@ -166,6 +176,18 @@
     }
 
     @Override
+    public RemovedArgumentInfo rewrittenWithLens(
+        AppView<AppInfoWithLiveness> appView, GraphLens graphLens) {
+      SingleValue rewrittenSingleValue =
+          hasSingleValue() ? singleValue.rewrittenWithLens(appView, graphLens) : null;
+      DexType rewrittenType = graphLens.lookupType(type);
+      if (rewrittenSingleValue != singleValue || rewrittenType != type) {
+        return new RemovedArgumentInfo(rewrittenSingleValue, rewrittenType);
+      }
+      return this;
+    }
+
+    @Override
     public boolean equals(Object obj) {
       if (obj == null || getClass() != obj.getClass()) {
         return false;
@@ -253,6 +275,19 @@
     }
 
     @Override
+    public RewrittenTypeInfo rewrittenWithLens(
+        AppView<AppInfoWithLiveness> appView, GraphLens graphLens) {
+      DexType rewrittenNewType = graphLens.lookupType(newType);
+      SingleValue rewrittenSingleValue =
+          hasSingleValue() ? getSingleValue().rewrittenWithLens(appView, graphLens) : null;
+      if (rewrittenNewType != newType || rewrittenSingleValue != singleValue) {
+        // The old type is intentionally not rewritten.
+        return new RewrittenTypeInfo(oldType, rewrittenNewType, rewrittenSingleValue);
+      }
+      return this;
+    }
+
+    @Override
     public boolean equals(Object obj) {
       if (obj == null || getClass() != obj.getClass()) {
         return false;
@@ -368,6 +403,28 @@
       return argumentInfos.size();
     }
 
+    public ArgumentInfoCollection rewrittenWithLens(
+        AppView<AppInfoWithLiveness> appView, GraphLens graphLens) {
+      Int2ObjectSortedMap<ArgumentInfo> rewrittenArgumentInfos = new Int2ObjectRBTreeMap<>();
+      for (Int2ObjectMap.Entry<ArgumentInfo> entry : argumentInfos.int2ObjectEntrySet()) {
+        ArgumentInfo argumentInfo = entry.getValue();
+        ArgumentInfo rewrittenArgumentInfo = argumentInfo.rewrittenWithLens(appView, graphLens);
+        if (rewrittenArgumentInfo != argumentInfo) {
+          rewrittenArgumentInfos.put(entry.getIntKey(), rewrittenArgumentInfo);
+        }
+      }
+      if (!rewrittenArgumentInfos.isEmpty()) {
+        for (Int2ObjectMap.Entry<ArgumentInfo> entry : argumentInfos.int2ObjectEntrySet()) {
+          int key = entry.getIntKey();
+          if (!rewrittenArgumentInfos.containsKey(key)) {
+            rewrittenArgumentInfos.put(key, entry.getValue());
+          }
+        }
+        return new ArgumentInfoCollection(rewrittenArgumentInfos);
+      }
+      return this;
+    }
+
     @Override
     public boolean equals(Object obj) {
       if (obj == null || getClass() != obj.getClass()) {
@@ -694,6 +751,20 @@
     return dexItemFactory.createProto(newReturnType, newParameters);
   }
 
+  public RewrittenPrototypeDescription rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens graphLens) {
+    ArgumentInfoCollection newArgumentInfoCollection =
+        argumentInfoCollection.rewrittenWithLens(appView, graphLens);
+    RewrittenTypeInfo newRewrittenReturnInfo =
+        hasRewrittenReturnInfo() ? rewrittenReturnInfo.rewrittenWithLens(appView, graphLens) : null;
+    if (newArgumentInfoCollection != argumentInfoCollection
+        || newRewrittenReturnInfo != rewrittenReturnInfo) {
+      return new RewrittenPrototypeDescription(
+          extraParameters, newRewrittenReturnInfo, newArgumentInfoCollection);
+    }
+    return this;
+  }
+
   public RewrittenPrototypeDescription withRewrittenReturnInfo(
       RewrittenTypeInfo newRewrittenReturnInfo) {
     if (Objects.equals(rewrittenReturnInfo, newRewrittenReturnInfo)) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
index 150f206..e5a1aec 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAccessAnalysis.java
@@ -85,8 +85,7 @@
               resolutionResult.asSuccessfulResolution().getResolutionPair().asProgramField();
           if (field != null) {
             if (fieldAssignmentTracker != null) {
-              fieldAssignmentTracker.recordFieldAccess(
-                  fieldInstruction, field.getDefinition(), code.context());
+              fieldAssignmentTracker.recordFieldAccess(fieldInstruction, field, code.context());
             }
             if (fieldBitAccessAnalysis != null) {
               fieldBitAccessAnalysis.recordFieldAccess(
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java
index 70c8519..714535c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java
@@ -4,19 +4,31 @@
 
 package com.android.tools.r8.ir.analysis.fieldaccess;
 
-import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldAccessInfo;
 import com.android.tools.r8.graph.FieldAccessInfoCollection;
 import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerUtils;
+import com.android.tools.r8.ir.analysis.fieldaccess.state.ConcreteArrayTypeFieldState;
+import com.android.tools.r8.ir.analysis.fieldaccess.state.ConcreteClassTypeFieldState;
+import com.android.tools.r8.ir.analysis.fieldaccess.state.ConcretePrimitiveTypeFieldState;
+import com.android.tools.r8.ir.analysis.fieldaccess.state.FieldState;
+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.DynamicTypeWithUpperBound;
+import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
 import com.android.tools.r8.ir.analysis.value.BottomValue;
 import com.android.tools.r8.ir.analysis.value.NonConstantNumberValue;
 import com.android.tools.r8.ir.analysis.value.SingleValue;
@@ -30,9 +42,10 @@
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldArgumentInitializationInfo;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoCollection;
+import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepFieldInfo;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayList;
@@ -40,13 +53,14 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 
 public class FieldAssignmentTracker {
 
+  private final AbstractValueFactory abstractValueFactory;
   private final AppView<AppInfoWithLiveness> appView;
+  private final DexItemFactory dexItemFactory;
 
   // A field access graph with edges from methods to the fields that they access. Edges are removed
   // from the graph as we process methods, such that we can conclude that all field writes have been
@@ -58,14 +72,17 @@
   // sites have been seen when a class no longer has any incoming edges.
   private final ObjectAllocationGraph objectAllocationGraph;
 
-  // The set of fields that may store a non-zero value.
-  private final Set<DexEncodedField> nonZeroFields = Sets.newConcurrentHashSet();
+  // Information about the fields in the program. If a field is not a key in the map then no writes
+  // has been seen to the field.
+  private final Map<DexEncodedField, FieldState> fieldStates = new ConcurrentHashMap<>();
 
   private final Map<DexProgramClass, Map<DexEncodedField, AbstractValue>>
       abstractInstanceFieldValues = new ConcurrentHashMap<>();
 
   FieldAssignmentTracker(AppView<AppInfoWithLiveness> appView) {
+    this.abstractValueFactory = appView.abstractValueFactory();
     this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
     this.fieldAccessGraph = new FieldAccessGraph();
     this.objectAllocationGraph = new ObjectAllocationGraph();
   }
@@ -109,33 +126,114 @@
           }
           abstractInstanceFieldValues.put(clazz, abstractInstanceFieldValuesForClass);
         });
-  }
-
-  private boolean isAlwaysZero(DexEncodedField field) {
-    return !appView.appInfo().isPinned(field.getReference()) && !nonZeroFields.contains(field);
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      clazz.forEachProgramField(
+          field -> {
+            FieldAccessInfo accessInfo = fieldAccessInfos.get(field.getReference());
+            KeepFieldInfo keepInfo = appView.getKeepInfo(field);
+            if (keepInfo.isPinned(appView.options()) || accessInfo.isWrittenFromMethodHandle()) {
+              fieldStates.put(field.getDefinition(), FieldState.unknown());
+            }
+          });
+    }
   }
 
   void acceptClassInitializerDefaultsResult(
       ClassInitializerDefaultsResult classInitializerDefaultsResult) {
     classInitializerDefaultsResult.forEachOptimizedField(
         (field, value) -> {
-          if (!value.isDefault(field.getReference().type)) {
-            nonZeroFields.add(field);
+          DexType fieldType = field.getType();
+          if (value.isDefault(field.getType())) {
+            return;
           }
+          assert fieldType.isClassType() || fieldType.isPrimitiveType();
+          fieldStates.compute(
+              field,
+              (f, fieldState) -> {
+                if (fieldState == null) {
+                  AbstractValue abstractValue = value.toAbstractValue(abstractValueFactory);
+                  if (fieldType.isClassType()) {
+                    assert abstractValue.isSingleStringValue()
+                        || abstractValue.isSingleDexItemBasedStringValue();
+                    if (fieldType == dexItemFactory.stringType) {
+                      return ConcreteClassTypeFieldState.create(
+                          abstractValue, DynamicType.definitelyNotNull());
+                    } else {
+                      ClassTypeElement nonNullableStringType =
+                          dexItemFactory
+                              .stringType
+                              .toTypeElement(appView, definitelyNotNull())
+                              .asClassType();
+                      return ConcreteClassTypeFieldState.create(
+                          abstractValue, DynamicType.createExact(nonNullableStringType));
+                    }
+                  } else {
+                    assert fieldType.isPrimitiveType();
+                    return ConcretePrimitiveTypeFieldState.create(abstractValue);
+                  }
+                }
+                // If the field is already assigned outside the class initializer then just give up.
+                return FieldState.unknown();
+              });
         });
   }
 
-  void recordFieldAccess(
-      FieldInstruction instruction, DexEncodedField field, ProgramMethod context) {
+  void recordFieldAccess(FieldInstruction instruction, ProgramField field, ProgramMethod context) {
     if (instruction.isFieldPut()) {
       recordFieldPut(field, instruction.value(), context);
     }
   }
 
-  private void recordFieldPut(DexEncodedField field, Value value, ProgramMethod context) {
-    if (!value.isZero()) {
-      nonZeroFields.add(field);
-    }
+  private void recordFieldPut(ProgramField field, Value value, ProgramMethod context) {
+    // For now only attempt to prove that fields are definitely null. In order to prove a single
+    // value for fields that are not definitely null, we need to prove that the given field is never
+    // read before it is written.
+    AbstractValue abstractValue =
+        value.isZero() ? abstractValueFactory.createZeroValue() : AbstractValue.unknown();
+    fieldStates.compute(
+        field.getDefinition(),
+        (f, fieldState) -> {
+          if (fieldState == null || fieldState.isBottom()) {
+            DexType fieldType = field.getType();
+            if (fieldType.isArrayType()) {
+              return ConcreteArrayTypeFieldState.create(abstractValue);
+            }
+            if (fieldType.isPrimitiveType()) {
+              return ConcretePrimitiveTypeFieldState.create(abstractValue);
+            }
+            assert fieldType.isClassType();
+            DynamicType dynamicType =
+                fieldType.isArrayType()
+                    ? DynamicType.unknown()
+                    : WideningUtils.widenDynamicNonReceiverType(
+                        appView,
+                        value.getDynamicType(appView).withNullability(Nullability.maybeNull()),
+                        field.getType());
+            return ConcreteClassTypeFieldState.create(abstractValue, dynamicType);
+          }
+
+          if (fieldState.isUnknown()) {
+            return fieldState;
+          }
+
+          assert fieldState.isConcrete();
+
+          if (fieldState.isArray()) {
+            ConcreteArrayTypeFieldState arrayFieldState = fieldState.asArray();
+            return arrayFieldState.mutableJoin(appView, abstractValue);
+          }
+
+          if (fieldState.isPrimitive()) {
+            ConcretePrimitiveTypeFieldState primitiveFieldState = fieldState.asPrimitive();
+            return primitiveFieldState.mutableJoin(abstractValue, abstractValueFactory);
+          }
+
+          assert fieldState.isClass();
+
+          ConcreteClassTypeFieldState classFieldState = fieldState.asClass();
+          return classFieldState.mutableJoin(
+              appView, abstractValue, value.getDynamicType(appView), field);
+        });
   }
 
   void recordAllocationSite(NewInstance instruction, DexProgramClass clazz, ProgramMethod context) {
@@ -146,7 +244,7 @@
       return;
     }
 
-    InvokeDirect invoke = instruction.getUniqueConstructorInvoke(appView.dexItemFactory());
+    InvokeDirect invoke = instruction.getUniqueConstructorInvoke(dexItemFactory);
     if (invoke == null) {
       // We just lost track.
       abstractInstanceFieldValues.remove(clazz);
@@ -238,27 +336,40 @@
   }
 
   private void recordAllFieldPutsProcessed(
-      DexEncodedField field, ProgramMethod context, OptimizationFeedbackDelayed feedback) {
-    DexProgramClass clazz = asProgramClassOrNull(appView.definitionForHolder(field, context));
-    if (clazz == null) {
-      assert false;
-      return;
+      ProgramField field, ProgramMethod context, OptimizationFeedbackDelayed feedback) {
+    FieldState fieldState = fieldStates.getOrDefault(field.getDefinition(), FieldState.bottom());
+    AbstractValue abstractValue = fieldState.getAbstractValue(appView.abstractValueFactory());
+    if (abstractValue.isNonTrivial()) {
+      feedback.recordFieldHasAbstractValue(field.getDefinition(), appView, abstractValue);
     }
 
-    if (isAlwaysZero(field)) {
-      feedback.recordFieldHasAbstractValue(
-          field, appView, appView.abstractValueFactory().createSingleNumberValue(0));
+    if (fieldState.isClass() && field.getOptimizationInfo().getDynamicType().isUnknown()) {
+      ConcreteClassTypeFieldState classFieldState = fieldState.asClass();
+      DynamicType dynamicType = classFieldState.getDynamicType();
+      if (!dynamicType.isUnknown()) {
+        assert WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, field.getType())
+            == dynamicType;
+        if (dynamicType.isNotNullType()) {
+          feedback.markFieldHasDynamicType(field.getDefinition(), dynamicType);
+        } else {
+          DynamicTypeWithUpperBound staticType = field.getType().toDynamicType(appView);
+          if (dynamicType.asDynamicTypeWithUpperBound().strictlyLessThan(staticType, appView)) {
+            feedback.markFieldHasDynamicType(field.getDefinition(), dynamicType);
+          }
+        }
+      }
     }
 
-    if (!field.isStatic()) {
-      recordAllInstanceFieldPutsProcessed(clazz, field, feedback);
+    if (!field.getAccessFlags().isStatic()) {
+      recordAllInstanceFieldPutsProcessed(field, feedback);
     }
   }
 
   private void recordAllInstanceFieldPutsProcessed(
-      DexProgramClass clazz, DexEncodedField field, OptimizationFeedbackDelayed feedback) {
+      ProgramField field, OptimizationFeedbackDelayed feedback) {
     if (appView.appInfo().isInstanceFieldWrittenOnlyInInstanceInitializers(field)) {
       AbstractValue abstractValue = BottomValue.getInstance();
+      DexProgramClass clazz = field.getHolder();
       for (DexEncodedMethod method : clazz.directMethods(DexEncodedMethod::isInstanceInitializer)) {
         InstanceFieldInitializationInfo fieldInitializationInfo =
             method
@@ -290,7 +401,7 @@
       assert !abstractValue.isBottom();
 
       if (!abstractValue.isUnknown()) {
-        feedback.recordFieldHasAbstractValue(field, appView, abstractValue);
+        feedback.recordFieldHasAbstractValue(field.getDefinition(), appView, abstractValue);
       }
     }
   }
@@ -334,8 +445,7 @@
   static class FieldAccessGraph {
 
     // The fields written by each method.
-    private final Map<DexEncodedMethod, List<DexEncodedField>> fieldWrites =
-        new IdentityHashMap<>();
+    private final Map<DexEncodedMethod, List<ProgramField>> fieldWrites = new IdentityHashMap<>();
 
     // The number of writes that have not yet been processed per field.
     private final Reference2IntMap<DexEncodedField> pendingFieldWrites =
@@ -348,8 +458,7 @@
           appView.appInfo().getFieldAccessInfoCollection();
       fieldAccessInfoCollection.forEach(
           info -> {
-            DexEncodedField field =
-                appView.appInfo().resolveField(info.getField()).getResolvedField();
+            ProgramField field = appView.appInfo().resolveField(info.getField()).getProgramField();
             if (field == null) {
               return;
             }
@@ -359,18 +468,18 @@
                       fieldWrites
                           .computeIfAbsent(context.getDefinition(), ignore -> new ArrayList<>())
                           .add(field));
-              pendingFieldWrites.put(field, info.getNumberOfWriteContexts());
+              pendingFieldWrites.put(field.getDefinition(), info.getNumberOfWriteContexts());
             }
           });
     }
 
-    void markProcessed(ProgramMethod method, Consumer<DexEncodedField> allWritesSeenConsumer) {
-      List<DexEncodedField> fieldWritesInMethod = fieldWrites.get(method.getDefinition());
+    void markProcessed(ProgramMethod method, Consumer<ProgramField> allWritesSeenConsumer) {
+      List<ProgramField> fieldWritesInMethod = fieldWrites.get(method.getDefinition());
       if (fieldWritesInMethod != null) {
-        for (DexEncodedField field : fieldWritesInMethod) {
-          int numberOfPendingFieldWrites = pendingFieldWrites.removeInt(field) - 1;
+        for (ProgramField field : fieldWritesInMethod) {
+          int numberOfPendingFieldWrites = pendingFieldWrites.removeInt(field.getDefinition()) - 1;
           if (numberOfPendingFieldWrites > 0) {
-            pendingFieldWrites.put(field, numberOfPendingFieldWrites);
+            pendingFieldWrites.put(field.getDefinition(), numberOfPendingFieldWrites);
           } else {
             allWritesSeenConsumer.accept(field);
           }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/BottomFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/BottomFieldState.java
new file mode 100644
index 0000000..c3474af
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/BottomFieldState.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldaccess.state;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+
+/** Used to represent the state for fields that have never been assigned in the program. */
+public class BottomFieldState extends FieldState {
+
+  private static final BottomFieldState INSTANCE = new BottomFieldState();
+
+  private BottomFieldState() {}
+
+  public static BottomFieldState getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public AbstractValue getAbstractValue(AbstractValueFactory abstractValueFactory) {
+    return abstractValueFactory.createNullValue();
+  }
+
+  @Override
+  public boolean isBottom() {
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteArrayTypeFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteArrayTypeFieldState.java
new file mode 100644
index 0000000..1b82558
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteArrayTypeFieldState.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldaccess.state;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+/**
+ * The information that we track for fields with an array type.
+ *
+ * <p>Since we don't gain much from tracking the dynamic types of arrays, this is only tracking the
+ * abstract value.
+ */
+public class ConcreteArrayTypeFieldState extends ConcreteReferenceTypeFieldState {
+
+  ConcreteArrayTypeFieldState(AbstractValue abstractValue) {
+    super(abstractValue);
+  }
+
+  public static FieldState create(AbstractValue abstractValue) {
+    return abstractValue.isUnknown()
+        ? FieldState.unknown()
+        : new ConcreteArrayTypeFieldState(abstractValue);
+  }
+
+  @Override
+  public boolean isArray() {
+    return true;
+  }
+
+  @Override
+  public ConcreteArrayTypeFieldState asArray() {
+    return this;
+  }
+
+  public FieldState mutableJoin(AppView<AppInfoWithLiveness> appView, AbstractValue abstractValue) {
+    this.abstractValue =
+        this.abstractValue.joinReference(abstractValue, appView.abstractValueFactory());
+    return isEffectivelyUnknown() ? unknown() : this;
+  }
+
+  private boolean isEffectivelyUnknown() {
+    return abstractValue.isUnknown();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteClassTypeFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteClassTypeFieldState.java
new file mode 100644
index 0000000..7cd6f2e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteClassTypeFieldState.java
@@ -0,0 +1,62 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldaccess.state;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+/** The information that we track for fields whose type is a class type. */
+public class ConcreteClassTypeFieldState extends ConcreteReferenceTypeFieldState {
+
+  private DynamicType dynamicType;
+
+  ConcreteClassTypeFieldState(AbstractValue abstractValue, DynamicType dynamicType) {
+    super(abstractValue);
+    this.dynamicType = dynamicType;
+  }
+
+  public static FieldState create(AbstractValue abstractValue, DynamicType dynamicType) {
+    return abstractValue.isUnknown() && dynamicType.isUnknown()
+        ? FieldState.unknown()
+        : new ConcreteClassTypeFieldState(abstractValue, dynamicType);
+  }
+
+  public DynamicType getDynamicType() {
+    return dynamicType;
+  }
+
+  @Override
+  public boolean isClass() {
+    return true;
+  }
+
+  @Override
+  public ConcreteClassTypeFieldState asClass() {
+    return this;
+  }
+
+  public FieldState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      AbstractValue abstractValue,
+      DynamicType dynamicType,
+      ProgramField field) {
+    this.abstractValue =
+        this.abstractValue.joinReference(abstractValue, appView.abstractValueFactory());
+    this.dynamicType =
+        field.getType().isArrayType()
+            ? DynamicType.unknown()
+            : WideningUtils.widenDynamicNonReceiverType(
+                appView, this.dynamicType.join(appView, dynamicType), field.getType());
+    return isEffectivelyUnknown() ? unknown() : this;
+  }
+
+  private boolean isEffectivelyUnknown() {
+    return abstractValue.isUnknown() && dynamicType.isUnknown();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteFieldState.java
new file mode 100644
index 0000000..25ddbb9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteFieldState.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldaccess.state;
+
+/** A shared base class for non-trivial field information (neither bottom nor top). */
+public abstract class ConcreteFieldState extends FieldState {
+
+  @Override
+  public boolean isConcrete() {
+    return true;
+  }
+
+  @Override
+  public ConcreteFieldState asConcrete() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcretePrimitiveTypeFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcretePrimitiveTypeFieldState.java
new file mode 100644
index 0000000..c2eb778
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcretePrimitiveTypeFieldState.java
@@ -0,0 +1,52 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldaccess.state;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+
+/** The information that we track for fields whose type is a primitive type. */
+public class ConcretePrimitiveTypeFieldState extends ConcreteFieldState {
+
+  private AbstractValue abstractValue;
+
+  ConcretePrimitiveTypeFieldState(AbstractValue abstractValue) {
+    this.abstractValue = abstractValue;
+  }
+
+  public static FieldState create(AbstractValue abstractValue) {
+    return abstractValue.isUnknown()
+        ? FieldState.unknown()
+        : new ConcretePrimitiveTypeFieldState(abstractValue);
+  }
+
+  @Override
+  public AbstractValue getAbstractValue(AbstractValueFactory abstractValueFactory) {
+    return abstractValue;
+  }
+
+  @Override
+  public boolean isPrimitive() {
+    return true;
+  }
+
+  @Override
+  public ConcretePrimitiveTypeFieldState asPrimitive() {
+    return this;
+  }
+
+  public FieldState mutableJoin(
+      AbstractValue abstractValue, AbstractValueFactory abstractValueFactory) {
+    if (abstractValue.isUnknown()) {
+      return FieldState.unknown();
+    }
+    this.abstractValue = this.abstractValue.joinPrimitive(abstractValue, abstractValueFactory);
+    return isEffectivelyUnknown() ? unknown() : this;
+  }
+
+  private boolean isEffectivelyUnknown() {
+    return abstractValue.isUnknown();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteReferenceTypeFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteReferenceTypeFieldState.java
new file mode 100644
index 0000000..64d0674
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/ConcreteReferenceTypeFieldState.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldaccess.state;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+
+/** The information that we track for fields whose type is a reference type. */
+public abstract class ConcreteReferenceTypeFieldState extends ConcreteFieldState {
+
+  protected AbstractValue abstractValue;
+
+  ConcreteReferenceTypeFieldState(AbstractValue abstractValue) {
+    this.abstractValue = abstractValue;
+  }
+
+  @Override
+  public AbstractValue getAbstractValue(AbstractValueFactory abstractValueFactory) {
+    return abstractValue;
+  }
+
+  @Override
+  public boolean isReference() {
+    return true;
+  }
+
+  @Override
+  public ConcreteReferenceTypeFieldState asReference() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/FieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/FieldState.java
new file mode 100644
index 0000000..6e0b203
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/FieldState.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldaccess.state;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+
+/** An abstraction of the runtime values that may flow into each field. */
+public abstract class FieldState {
+
+  public static BottomFieldState bottom() {
+    return BottomFieldState.getInstance();
+  }
+
+  public static UnknownFieldState unknown() {
+    return UnknownFieldState.getInstance();
+  }
+
+  public abstract AbstractValue getAbstractValue(AbstractValueFactory abstractValueFactory);
+
+  public boolean isArray() {
+    return false;
+  }
+
+  public ConcreteArrayTypeFieldState asArray() {
+    return null;
+  }
+
+  public boolean isBottom() {
+    return false;
+  }
+
+  public boolean isClass() {
+    return false;
+  }
+
+  public ConcreteClassTypeFieldState asClass() {
+    return null;
+  }
+
+  public boolean isConcrete() {
+    return false;
+  }
+
+  public ConcreteFieldState asConcrete() {
+    return null;
+  }
+
+  public boolean isPrimitive() {
+    return false;
+  }
+
+  public ConcretePrimitiveTypeFieldState asPrimitive() {
+    return null;
+  }
+
+  public boolean isReference() {
+    return false;
+  }
+
+  public ConcreteReferenceTypeFieldState asReference() {
+    return null;
+  }
+
+  public boolean isUnknown() {
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/UnknownFieldState.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/UnknownFieldState.java
new file mode 100644
index 0000000..e408f09
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/state/UnknownFieldState.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldaccess.state;
+
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+
+/** Represents that nothing is known about the values that may flow into a given field. */
+public class UnknownFieldState extends FieldState {
+
+  private static final UnknownFieldState INSTANCE = new UnknownFieldState();
+
+  private UnknownFieldState() {}
+
+  public static UnknownFieldState getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public AbstractValue getAbstractValue(AbstractValueFactory abstractValueFactory) {
+    return AbstractValue.unknown();
+  }
+
+  @Override
+  public boolean isUnknown() {
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
index 0303ba7..30c3b67 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
@@ -14,9 +14,8 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.DexValue.DexValueNull;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+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.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
 import com.android.tools.r8.ir.analysis.value.NullOrAbstractValue;
@@ -147,26 +146,17 @@
     // Abstract value.
     feedback.recordFieldHasAbstractValue(field, appView, abstractValue);
 
-    // Dynamic upper bound type.
-    TypeElement fieldType =
-        TypeElement.fromDexType(field.getReference().type, Nullability.maybeNull(), appView);
-    TypeElement dynamicUpperBoundType = value.getDynamicUpperBoundType(appView);
-    if (dynamicUpperBoundType.strictlyLessThan(fieldType, appView)) {
-      if (maybeNull && dynamicUpperBoundType.isDefinitelyNotNull()) {
-        assert dynamicUpperBoundType.isReferenceType();
-        dynamicUpperBoundType = dynamicUpperBoundType.asReferenceType().asMaybeNull();
+    // Dynamic type.
+    if (field.getType().isReferenceType()) {
+      DynamicTypeWithUpperBound staticType = field.getType().toDynamicType(appView);
+      DynamicTypeWithUpperBound dynamicType = value.getDynamicType(appView);
+      if (dynamicType.strictlyLessThan(staticType, appView)) {
+        if (maybeNull && dynamicType.getNullability().isDefinitelyNotNull()) {
+          assert dynamicType.getDynamicUpperBoundType().isReferenceType();
+          dynamicType = dynamicType.withNullability(Nullability.maybeNull());
+        }
+        feedback.markFieldHasDynamicType(field, dynamicType);
       }
-      feedback.markFieldHasDynamicUpperBoundType(field, dynamicUpperBoundType);
-    }
-
-    // Dynamic lower bound type.
-    ClassTypeElement dynamicLowerBoundType = value.getDynamicLowerBoundType(appView);
-    if (dynamicLowerBoundType != null) {
-      assert dynamicLowerBoundType.lessThanOrEqual(dynamicUpperBoundType, appView);
-      if (maybeNull && dynamicLowerBoundType.isDefinitelyNotNull()) {
-        dynamicLowerBoundType = dynamicLowerBoundType.asMaybeNull().asClassType();
-      }
-      feedback.markFieldHasDynamicLowerBoundType(field, dynamicLowerBoundType);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicType.java b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicType.java
index 1a32390..1b9bb1e 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicType.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicType.java
@@ -6,9 +6,11 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import java.util.Objects;
+import java.util.Set;
 
 /**
  * Represents the runtime type of a reference value. This type may be more precise than the value's
@@ -17,20 +19,9 @@
  * <p>If a lower bound is known on the runtime type (e.g., {@code new A()}), then {@link
  * DynamicTypeWithLowerBound} is used.
  */
-public class DynamicType {
+public abstract class DynamicType {
 
-  private static final DynamicType BOTTOM = new DynamicType(TypeElement.getBottom());
-  private static final DynamicType NULL_TYPE = new DynamicType(TypeElement.getNull());
-  private static final DynamicType UNKNOWN = new DynamicType(TypeElement.getTop());
-
-  private final TypeElement dynamicUpperBoundType;
-
-  DynamicType(TypeElement dynamicUpperBoundType) {
-    assert dynamicUpperBoundType != null;
-    this.dynamicUpperBoundType = dynamicUpperBoundType;
-  }
-
-  public static DynamicType create(
+  public static DynamicTypeWithUpperBound create(
       AppView<AppInfoWithLiveness> appView, TypeElement dynamicUpperBoundType) {
     ClassTypeElement dynamicLowerBoundType = null;
     if (dynamicUpperBoundType.isClassType()) {
@@ -44,7 +35,7 @@
     return create(appView, dynamicUpperBoundType, dynamicLowerBoundType);
   }
 
-  public static DynamicType create(
+  public static DynamicTypeWithUpperBound create(
       AppView<AppInfoWithLiveness> appView,
       TypeElement dynamicUpperBoundType,
       ClassTypeElement dynamicLowerBoundType) {
@@ -67,14 +58,15 @@
           appView, dynamicUpperBoundType.asClassType(), dynamicLowerBoundType);
     }
     assert verifyNotEffectivelyFinalClassType(appView, dynamicUpperBoundType);
-    return new DynamicType(dynamicUpperBoundType);
+    return new DynamicTypeWithUpperBound(dynamicUpperBoundType);
   }
 
-  public static DynamicType createExact(ClassTypeElement exactDynamicType) {
+  public static ExactDynamicType createExact(ClassTypeElement exactDynamicType) {
     return new ExactDynamicType(exactDynamicType);
   }
 
-  public static DynamicType create(AppView<AppInfoWithLiveness> appView, Value value) {
+  public static DynamicTypeWithUpperBound create(
+      AppView<AppInfoWithLiveness> appView, Value value) {
     assert value.getType().isReferenceType();
     TypeElement dynamicUpperBoundType = value.getDynamicUpperBoundType(appView);
     ClassTypeElement dynamicLowerBoundType =
@@ -83,22 +75,46 @@
     return create(appView, dynamicUpperBoundType, dynamicLowerBoundType);
   }
 
-  public static DynamicType bottom() {
-    return BOTTOM;
+  public static DynamicTypeWithUpperBound bottom() {
+    return DynamicTypeWithUpperBound.BOTTOM;
   }
 
-  public static DynamicType definitelyNull() {
-    return NULL_TYPE;
+  public static DynamicTypeWithUpperBound definitelyNull() {
+    return DynamicTypeWithUpperBound.NULL_TYPE;
   }
 
-  public static DynamicType unknown() {
-    return UNKNOWN;
+  public static NotNullDynamicType definitelyNotNull() {
+    return NotNullDynamicType.get();
   }
 
-  public TypeElement getDynamicUpperBoundType() {
-    return dynamicUpperBoundType;
+  public static DynamicTypeWithUpperBound unknown() {
+    return DynamicTypeWithUpperBound.UNKNOWN;
   }
 
+  public static DynamicType join(
+      AppView<AppInfoWithLiveness> appView, Iterable<DynamicType> dynamicTypes) {
+    DynamicType result = bottom();
+    for (DynamicType dynamicType : dynamicTypes) {
+      result = result.join(appView, dynamicType);
+    }
+    return result;
+  }
+
+  public boolean hasDynamicUpperBoundType() {
+    return false;
+  }
+
+  /**
+   * Returns the dynamic upper bound type if this is an instance of {@link
+   * DynamicTypeWithUpperBound}.
+   *
+   * <p>The {@link NotNullDynamicType} does not have an upper bound type. This therefore takes the
+   * static type corresponding to the dynamic type as an argument, and returns the given static type
+   * with non null information attached to it when this dynamic type is the {@link
+   * NotNullDynamicType}.
+   */
+  public abstract TypeElement getDynamicUpperBoundType(TypeElement staticType);
+
   public boolean hasDynamicLowerBoundType() {
     return false;
   }
@@ -107,20 +123,36 @@
     return null;
   }
 
-  public Nullability getNullability() {
-    return getDynamicUpperBoundType().nullability();
-  }
+  public abstract ClassTypeElement getExactClassType();
+
+  public abstract Nullability getNullability();
 
   public boolean isBottom() {
-    return getDynamicUpperBoundType().isBottom();
+    return false;
+  }
+
+  public boolean isDynamicTypeWithUpperBound() {
+    return false;
+  }
+
+  public DynamicTypeWithUpperBound asDynamicTypeWithUpperBound() {
+    return null;
+  }
+
+  public boolean isExactClassType() {
+    return getExactClassType() != null;
   }
 
   public boolean isNullType() {
-    return getDynamicUpperBoundType().isNullType();
+    return false;
+  }
+
+  public boolean isNotNullType() {
+    return false;
   }
 
   public boolean isUnknown() {
-    return getDynamicUpperBoundType().isTop();
+    return false;
   }
 
   public DynamicType join(AppView<AppInfoWithLiveness> appView, DynamicType dynamicType) {
@@ -133,57 +165,27 @@
     if (isUnknown() || dynamicType.isUnknown()) {
       return unknown();
     }
-    TypeElement upperBoundType =
-        getDynamicUpperBoundType().join(dynamicType.getDynamicUpperBoundType(), appView);
-    ClassTypeElement lowerBoundType = meetDynamicLowerBound(appView, dynamicType);
-    if (upperBoundType.equals(getDynamicUpperBoundType())
-        && Objects.equals(lowerBoundType, getDynamicLowerBoundType())) {
-      return this;
+    if (isNotNullType() || dynamicType.isNotNullType()) {
+      if (getNullability().isNullable() || dynamicType.getNullability().isNullable()) {
+        return unknown();
+      }
+      return definitelyNotNull();
     }
-    return create(appView, upperBoundType, lowerBoundType);
+    assert isDynamicTypeWithUpperBound();
+    assert dynamicType.isDynamicTypeWithUpperBound();
+    return asDynamicTypeWithUpperBound().join(appView, dynamicType.asDynamicTypeWithUpperBound());
   }
 
-  private ClassTypeElement meetDynamicLowerBound(
-      AppView<AppInfoWithLiveness> appView, DynamicType dynamicType) {
-    if (isNullType()) {
-      if (dynamicType.hasDynamicLowerBoundType()) {
-        return dynamicType.getDynamicLowerBoundType().joinNullability(Nullability.definitelyNull());
-      }
-      return null;
-    }
-    if (dynamicType.isNullType()) {
-      if (hasDynamicLowerBoundType()) {
-        return getDynamicLowerBoundType().joinNullability(Nullability.definitelyNull());
-      }
-      return null;
-    }
-    if (!hasDynamicLowerBoundType() || !dynamicType.hasDynamicLowerBoundType()) {
-      return null;
-    }
-    ClassTypeElement lowerBoundType = getDynamicLowerBoundType();
-    ClassTypeElement otherLowerBoundType = dynamicType.getDynamicLowerBoundType();
-    if (lowerBoundType.lessThanOrEqualUpToNullability(otherLowerBoundType, appView)) {
-      return lowerBoundType.joinNullability(otherLowerBoundType.nullability());
-    }
-    if (otherLowerBoundType.lessThanOrEqualUpToNullability(lowerBoundType, appView)) {
-      return otherLowerBoundType.joinNullability(lowerBoundType.nullability());
-    }
-    return null;
-  }
+  public abstract DynamicType rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens graphLens, Set<DexType> prunedTypes);
+
+  public abstract DynamicType withNullability(Nullability nullability);
 
   @Override
-  public boolean equals(Object other) {
-    if (other == null || getClass() != other.getClass()) {
-      return false;
-    }
-    DynamicType dynamicType = (DynamicType) other;
-    return dynamicUpperBoundType.equals(dynamicType.dynamicUpperBoundType);
-  }
+  public abstract boolean equals(Object other);
 
   @Override
-  public int hashCode() {
-    return dynamicUpperBoundType.hashCode();
-  }
+  public abstract int hashCode();
 
   private static boolean verifyNotEffectivelyFinalClassType(
       AppView<AppInfoWithLiveness> appView, TypeElement type) {
@@ -194,17 +196,4 @@
     }
     return true;
   }
-
-  public DynamicType withNullability(Nullability nullability) {
-    assert !hasDynamicLowerBoundType();
-    if (!getDynamicUpperBoundType().isReferenceType()) {
-      return this;
-    }
-    ReferenceTypeElement dynamicUpperBoundReferenceType =
-        getDynamicUpperBoundType().asReferenceType();
-    if (dynamicUpperBoundReferenceType.nullability() == nullability) {
-      return this;
-    }
-    return new DynamicType(dynamicUpperBoundReferenceType.getOrCreateVariant(nullability));
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithLowerBound.java b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithLowerBound.java
index e4190bc..9448ebb 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithLowerBound.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithLowerBound.java
@@ -8,7 +8,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Objects;
 
-public class DynamicTypeWithLowerBound extends DynamicType {
+public class DynamicTypeWithLowerBound extends DynamicTypeWithUpperBound {
 
   private final ClassTypeElement dynamicLowerBoundType;
 
@@ -16,6 +16,7 @@
       ClassTypeElement dynamicUpperBoundType, ClassTypeElement dynamicLowerBoundType) {
     super(dynamicUpperBoundType);
     assert !dynamicUpperBoundType.equals(dynamicLowerBoundType);
+    assert dynamicUpperBoundType.nullability() == dynamicLowerBoundType.nullability();
     this.dynamicLowerBoundType = dynamicLowerBoundType;
   }
 
@@ -64,7 +65,7 @@
   }
 
   @Override
-  public DynamicType withNullability(Nullability nullability) {
+  public DynamicTypeWithLowerBound withNullability(Nullability nullability) {
     if (getDynamicUpperBoundType().nullability() == nullability) {
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithUpperBound.java b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithUpperBound.java
new file mode 100644
index 0000000..fa985fa
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithUpperBound.java
@@ -0,0 +1,283 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.type;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents the runtime type of a reference value. This type may be more precise than the value's
+ * statically declared type.
+ *
+ * <p>If a lower bound is known on the runtime type (e.g., {@code new A()}), then {@link
+ * DynamicTypeWithLowerBound} is used.
+ */
+public class DynamicTypeWithUpperBound extends DynamicType {
+
+  static final DynamicTypeWithUpperBound BOTTOM =
+      new DynamicTypeWithUpperBound(TypeElement.getBottom());
+  static final DynamicTypeWithUpperBound NULL_TYPE =
+      new DynamicTypeWithUpperBound(TypeElement.getNull());
+  static final DynamicTypeWithUpperBound UNKNOWN =
+      new DynamicTypeWithUpperBound(TypeElement.getTop());
+
+  private final TypeElement dynamicUpperBoundType;
+
+  DynamicTypeWithUpperBound(TypeElement dynamicUpperBoundType) {
+    assert dynamicUpperBoundType != null;
+    this.dynamicUpperBoundType = dynamicUpperBoundType;
+  }
+
+  public static DynamicTypeWithUpperBound create(
+      AppView<AppInfoWithLiveness> appView, TypeElement dynamicUpperBoundType) {
+    ClassTypeElement dynamicLowerBoundType =
+        isEffectivelyFinal(appView, dynamicUpperBoundType)
+            ? dynamicUpperBoundType.asClassType()
+            : null;
+    return create(appView, dynamicUpperBoundType, dynamicLowerBoundType);
+  }
+
+  public static DynamicTypeWithUpperBound create(
+      AppView<AppInfoWithLiveness> appView,
+      TypeElement dynamicUpperBoundType,
+      ClassTypeElement dynamicLowerBoundType) {
+    if (dynamicUpperBoundType.isBottom()) {
+      return bottom();
+    }
+    if (dynamicUpperBoundType.isNullType()) {
+      return definitelyNull();
+    }
+    if (dynamicUpperBoundType.isTop()) {
+      return unknown();
+    }
+    if (dynamicLowerBoundType != null) {
+      assert dynamicUpperBoundType.isClassType();
+      assert dynamicUpperBoundType.nullability() == dynamicLowerBoundType.nullability();
+      if (dynamicUpperBoundType.equals(dynamicLowerBoundType)) {
+        return createExact(dynamicLowerBoundType);
+      }
+      return DynamicTypeWithLowerBound.create(
+          appView, dynamicUpperBoundType.asClassType(), dynamicLowerBoundType);
+    }
+    assert verifyNotEffectivelyFinalClassType(appView, dynamicUpperBoundType);
+    return new DynamicTypeWithUpperBound(dynamicUpperBoundType);
+  }
+
+  public static DynamicTypeWithUpperBound create(
+      AppView<AppInfoWithLiveness> appView, Value value) {
+    assert value.getType().isReferenceType();
+    TypeElement dynamicUpperBoundType = value.getDynamicUpperBoundType(appView);
+    ClassTypeElement dynamicLowerBoundType =
+        value.getDynamicLowerBoundType(
+            appView, dynamicUpperBoundType, dynamicUpperBoundType.nullability());
+    return create(appView, dynamicUpperBoundType, dynamicLowerBoundType);
+  }
+
+  private static boolean isEffectivelyFinal(AppView<?> appView, TypeElement type) {
+    if (type.isClassType()) {
+      ClassTypeElement classType = type.asClassType();
+      DexClass clazz = appView.definitionFor(classType.getClassType());
+      return clazz != null && clazz.isEffectivelyFinal(appView);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean hasDynamicUpperBoundType() {
+    return true;
+  }
+
+  @Override
+  public TypeElement getDynamicUpperBoundType(TypeElement staticType) {
+    return getDynamicUpperBoundType();
+  }
+
+  public TypeElement getDynamicUpperBoundType() {
+    return dynamicUpperBoundType;
+  }
+
+  @Override
+  public boolean hasDynamicLowerBoundType() {
+    return false;
+  }
+
+  @Override
+  public ClassTypeElement getDynamicLowerBoundType() {
+    return null;
+  }
+
+  @Override
+  public boolean isExactClassType() {
+    return getExactClassType() != null;
+  }
+
+  @Override
+  public ClassTypeElement getExactClassType() {
+    return hasDynamicLowerBoundType()
+            && getDynamicLowerBoundType().equalUpToNullability(getDynamicUpperBoundType())
+        ? getDynamicLowerBoundType()
+        : null;
+  }
+
+  @Override
+  public Nullability getNullability() {
+    return dynamicUpperBoundType.nullability();
+  }
+
+  @Override
+  public boolean isBottom() {
+    return dynamicUpperBoundType.isBottom();
+  }
+
+  @Override
+  public boolean isDynamicTypeWithUpperBound() {
+    return true;
+  }
+
+  @Override
+  public DynamicTypeWithUpperBound asDynamicTypeWithUpperBound() {
+    return this;
+  }
+
+  @Override
+  public boolean isNullType() {
+    return dynamicUpperBoundType.isNullType();
+  }
+
+  @Override
+  public boolean isUnknown() {
+    return dynamicUpperBoundType.isTop();
+  }
+
+  public DynamicType join(
+      AppView<AppInfoWithLiveness> appView, DynamicTypeWithUpperBound dynamicType) {
+    TypeElement upperBoundType =
+        getDynamicUpperBoundType().join(dynamicType.getDynamicUpperBoundType(), appView);
+    ClassTypeElement lowerBoundType =
+        isEffectivelyFinal(appView, upperBoundType)
+            ? upperBoundType.asClassType()
+            : meetDynamicLowerBound(appView, dynamicType);
+    if (upperBoundType.equals(getDynamicUpperBoundType())
+        && Objects.equals(lowerBoundType, getDynamicLowerBoundType())) {
+      return this;
+    }
+    return create(appView, upperBoundType, lowerBoundType);
+  }
+
+  private ClassTypeElement meetDynamicLowerBound(
+      AppView<AppInfoWithLiveness> appView, DynamicType dynamicType) {
+    if (isNullType()) {
+      if (dynamicType.hasDynamicLowerBoundType()) {
+        return dynamicType.getDynamicLowerBoundType().joinNullability(Nullability.definitelyNull());
+      }
+      return null;
+    }
+    if (dynamicType.isNullType()) {
+      if (hasDynamicLowerBoundType()) {
+        return getDynamicLowerBoundType().joinNullability(Nullability.definitelyNull());
+      }
+      return null;
+    }
+    if (!hasDynamicLowerBoundType() || !dynamicType.hasDynamicLowerBoundType()) {
+      return null;
+    }
+    ClassTypeElement lowerBoundType = getDynamicLowerBoundType();
+    ClassTypeElement otherLowerBoundType = dynamicType.getDynamicLowerBoundType();
+    if (lowerBoundType.lessThanOrEqualUpToNullability(otherLowerBoundType, appView)) {
+      return lowerBoundType.joinNullability(otherLowerBoundType.nullability());
+    }
+    if (otherLowerBoundType.lessThanOrEqualUpToNullability(lowerBoundType, appView)) {
+      return otherLowerBoundType.joinNullability(lowerBoundType.nullability());
+    }
+    return null;
+  }
+
+  @Override
+  public DynamicType rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens graphLens, Set<DexType> prunedTypes) {
+    if (isBottom() || isNullType() || isUnknown()) {
+      return this;
+    }
+    TypeElement rewrittenDynamicUpperBoundType =
+        dynamicUpperBoundType.rewrittenWithLens(appView, graphLens, prunedTypes);
+    ClassTypeElement rewrittenDynamicLowerBoundClassType = null;
+    if (hasDynamicLowerBoundType()) {
+      TypeElement rewrittenDynamicLowerBoundType =
+          getDynamicLowerBoundType().rewrittenWithLens(appView, graphLens, prunedTypes);
+      if (rewrittenDynamicLowerBoundType.isClassType()) {
+        rewrittenDynamicLowerBoundClassType = rewrittenDynamicLowerBoundType.asClassType();
+      }
+    }
+    return rewrittenDynamicLowerBoundClassType != null
+        ? create(appView, rewrittenDynamicUpperBoundType, rewrittenDynamicLowerBoundClassType)
+        : create(appView, rewrittenDynamicUpperBoundType);
+  }
+
+  public boolean strictlyLessThan(TypeElement type, AppView<AppInfoWithLiveness> appView) {
+    DynamicTypeWithUpperBound dynamicType = create(appView, type);
+    return strictlyLessThan(dynamicType, appView);
+  }
+
+  public boolean strictlyLessThan(DynamicTypeWithUpperBound dynamicType, AppView<?> appView) {
+    if (equals(dynamicType)) {
+      return false;
+    }
+    if (getDynamicUpperBoundType().equals(dynamicType.getDynamicUpperBoundType())) {
+      if (!dynamicType.hasDynamicLowerBoundType()) {
+        return hasDynamicLowerBoundType();
+      }
+      return hasDynamicLowerBoundType()
+          && getDynamicLowerBoundType()
+              .strictlyLessThan(dynamicType.getDynamicLowerBoundType(), appView);
+    }
+    return getDynamicUpperBoundType()
+        .strictlyLessThan(dynamicType.getDynamicUpperBoundType(), appView);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+    DynamicTypeWithUpperBound dynamicType = (DynamicTypeWithUpperBound) other;
+    return dynamicUpperBoundType.equals(dynamicType.dynamicUpperBoundType);
+  }
+
+  @Override
+  public int hashCode() {
+    return dynamicUpperBoundType.hashCode();
+  }
+
+  private static boolean verifyNotEffectivelyFinalClassType(
+      AppView<AppInfoWithLiveness> appView, TypeElement type) {
+    if (type.isClassType()) {
+      ClassTypeElement classType = type.asClassType();
+      DexClass clazz = appView.definitionFor(classType.getClassType());
+      assert clazz == null || !clazz.isEffectivelyFinal(appView);
+    }
+    return true;
+  }
+
+  @Override
+  public DynamicTypeWithUpperBound withNullability(Nullability nullability) {
+    assert !hasDynamicLowerBoundType();
+    if (!getDynamicUpperBoundType().isReferenceType()) {
+      return this;
+    }
+    ReferenceTypeElement dynamicUpperBoundReferenceType =
+        getDynamicUpperBoundType().asReferenceType();
+    if (dynamicUpperBoundReferenceType.nullability() == nullability) {
+      return this;
+    }
+    return new DynamicTypeWithUpperBound(
+        dynamicUpperBoundReferenceType.getOrCreateVariant(nullability));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ExactDynamicType.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ExactDynamicType.java
index d66cfcc..1ab6c53 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ExactDynamicType.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ExactDynamicType.java
@@ -4,25 +4,60 @@
 
 package com.android.tools.r8.ir.analysis.type;
 
-public class ExactDynamicType extends DynamicType {
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Set;
+
+public class ExactDynamicType extends DynamicTypeWithUpperBound {
 
   ExactDynamicType(ClassTypeElement exactDynamicType) {
     super(exactDynamicType);
   }
 
   @Override
+  public ClassTypeElement getDynamicUpperBoundType() {
+    return getExactClassType();
+  }
+
+  @Override
+  public ClassTypeElement getDynamicLowerBoundType() {
+    return getExactClassType();
+  }
+
+  @Override
+  public ClassTypeElement getExactClassType() {
+    return super.getDynamicUpperBoundType().asClassType();
+  }
+
+  @Override
   public boolean hasDynamicLowerBoundType() {
     return true;
   }
 
   @Override
-  public ClassTypeElement getDynamicUpperBoundType() {
-    return super.getDynamicUpperBoundType().asClassType();
+  public boolean isExactClassType() {
+    return true;
   }
 
   @Override
-  public ClassTypeElement getDynamicLowerBoundType() {
-    return getDynamicUpperBoundType();
+  public DynamicType rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens graphLens, Set<DexType> prunedTypes) {
+    TypeElement rewrittenType =
+        getExactClassType().rewrittenWithLens(appView, graphLens, prunedTypes);
+    assert rewrittenType.isClassType() || rewrittenType.isPrimitiveType();
+    return rewrittenType.isClassType()
+        ? new ExactDynamicType(rewrittenType.asClassType())
+        : unknown();
+  }
+
+  @Override
+  public ExactDynamicType withNullability(Nullability nullability) {
+    if (getNullability() == nullability) {
+      return this;
+    }
+    return new ExactDynamicType(getExactClassType().getOrCreateVariant(nullability));
   }
 
   @Override
@@ -31,19 +66,11 @@
       return false;
     }
     ExactDynamicType dynamicType = (ExactDynamicType) other;
-    return getDynamicUpperBoundType().equals(dynamicType.getDynamicUpperBoundType());
+    return getExactClassType().equals(dynamicType.getExactClassType());
   }
 
   @Override
   public int hashCode() {
-    return getDynamicLowerBoundType().hashCode();
-  }
-
-  @Override
-  public DynamicType withNullability(Nullability nullability) {
-    if (getDynamicUpperBoundType().nullability() == nullability) {
-      return this;
-    }
-    return new ExactDynamicType(getDynamicUpperBoundType().getOrCreateVariant(nullability));
+    return getExactClassType().hashCode();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/NotNullDynamicType.java b/src/main/java/com/android/tools/r8/ir/analysis/type/NotNullDynamicType.java
new file mode 100644
index 0000000..37ed14a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/NotNullDynamicType.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.type;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Set;
+
+/**
+ * A dynamic type that encodes that a given value is guaranteed to be non null.
+ *
+ * <p>This dynamic type is a singleton and does not have an upper bound type. If this dynamic type
+ * is used in a context where there is a corresponding static type, then the dynamic upper bound
+ * type is the static type with non null information attached. See also {@link
+ * #getDynamicUpperBoundType(TypeElement)}.
+ */
+public class NotNullDynamicType extends DynamicType {
+
+  private static final NotNullDynamicType INSTANCE = new NotNullDynamicType();
+
+  private NotNullDynamicType() {}
+
+  public static NotNullDynamicType get() {
+    return INSTANCE;
+  }
+
+  @Override
+  public ReferenceTypeElement getDynamicUpperBoundType(TypeElement staticType) {
+    assert staticType.isReferenceType();
+    return staticType.asReferenceType().getOrCreateVariant(Nullability.definitelyNotNull());
+  }
+
+  @Override
+  public ClassTypeElement getExactClassType() {
+    return null;
+  }
+
+  @Override
+  public Nullability getNullability() {
+    return Nullability.definitelyNotNull();
+  }
+
+  @Override
+  public boolean isNotNullType() {
+    return true;
+  }
+
+  @Override
+  public NotNullDynamicType rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens graphLens, Set<DexType> prunedTypes) {
+    return this;
+  }
+
+  @Override
+  public DynamicType withNullability(Nullability nullability) {
+    assert !nullability.isBottom();
+    return nullability.isDefinitelyNotNull() ? this : unknown();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    return this == other;
+  }
+
+  @Override
+  public int hashCode() {
+    return System.identityHashCode(this);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
index 854ea6d..850b669 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
@@ -180,6 +180,14 @@
     return join(other, factory, type.isReferenceType(), false);
   }
 
+  public AbstractValue joinPrimitive(AbstractValue other, AbstractValueFactory factory) {
+    return join(other, factory, false, false);
+  }
+
+  public AbstractValue joinReference(AbstractValue other, AbstractValueFactory factory) {
+    return join(other, factory, true, false);
+  }
+
   // TODO(b/196321452): Clean this up, in particular, replace the "allow" parameters by a
   //  configuration object.
   public AbstractValue join(
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java
index b499b81..0e746d4 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java
@@ -143,7 +143,11 @@
         return factory.createSingleNumberValue(enumDataMap.getUnboxedValue(field));
       }
     }
-    return factory.createSingleFieldValue(
-        lens.lookupField(field), getObjectState().rewrittenWithLens(appView, lens));
+    DexField rewrittenField = lens.lookupField(field);
+    ObjectState rewrittenObjectState = getObjectState().rewrittenWithLens(appView, lens);
+    if (rewrittenField != field || rewrittenObjectState != getObjectState()) {
+      return factory.createSingleFieldValue(rewrittenField, rewrittenObjectState);
+    }
+    return this;
   }
 }
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 f25c73b..e0a6238 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,7 +10,7 @@
 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.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
 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;
@@ -53,19 +53,12 @@
   }
 
   public static Assume createAssumeDynamicTypeInstruction(
-      TypeElement dynamicUpperBoundType,
-      ClassTypeElement dynamicLowerBoundType,
+      DynamicTypeWithUpperBound dynamicType,
       Value dest,
       Value src,
       Instruction origin,
       AppView<?> appView) {
-    return new Assume(
-        new DynamicTypeAssumption(dynamicUpperBoundType, dynamicLowerBoundType),
-        null,
-        dest,
-        src,
-        origin,
-        appView);
+    return new Assume(new DynamicTypeAssumption(dynamicType), null, dest, src, origin, appView);
   }
 
   @Override
@@ -142,7 +135,7 @@
       return false;
     }
     if (hasDynamicTypeAssumption()) {
-      outType = dynamicTypeAssumption.getDynamicUpperBoundType();
+      outType = dynamicTypeAssumption.getDynamicType().getDynamicUpperBoundType();
     }
     if (appView.appInfo().hasLiveness()) {
       if (outType.isClassType()
@@ -261,6 +254,8 @@
       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;
     }
@@ -279,13 +274,14 @@
       builder.append("; not null");
     }
     if (hasDynamicTypeAssumption()) {
+      DynamicTypeWithUpperBound dynamicType = dynamicTypeAssumption.getDynamicType();
       if (hasOutValue()) {
-        if (!dynamicTypeAssumption.dynamicUpperBoundType.equalUpToNullability(outValue.getType())) {
-          builder.append("; upper bound: ").append(dynamicTypeAssumption.dynamicUpperBoundType);
+        if (!dynamicType.getDynamicUpperBoundType().equalUpToNullability(outValue.getType())) {
+          builder.append("; upper bound: ").append(dynamicType.getDynamicUpperBoundType());
         }
       }
-      if (dynamicTypeAssumption.dynamicLowerBoundType != null) {
-        builder.append("; lower bound: ").append(dynamicTypeAssumption.dynamicLowerBoundType);
+      if (dynamicType.hasDynamicLowerBoundType()) {
+        builder.append("; lower bound: ").append(dynamicType.getDynamicLowerBoundType());
       }
     }
     return builder.toString();
@@ -293,27 +289,23 @@
 
   public static class DynamicTypeAssumption {
 
-    private final TypeElement dynamicUpperBoundType;
-    private final ClassTypeElement dynamicLowerBoundType;
+    private final DynamicTypeWithUpperBound dynamicType;
 
-    public DynamicTypeAssumption(
-        TypeElement dynamicUpperBoundType, ClassTypeElement dynamicLowerBoundType) {
-      this.dynamicUpperBoundType = dynamicUpperBoundType;
-      this.dynamicLowerBoundType = dynamicLowerBoundType;
+    public DynamicTypeAssumption(DynamicTypeWithUpperBound dynamicType) {
+      assert dynamicType != null;
+      this.dynamicType = dynamicType;
     }
 
-    public TypeElement getDynamicUpperBoundType() {
-      return dynamicUpperBoundType;
-    }
-
-    public ClassTypeElement getDynamicLowerBoundType() {
-      return dynamicLowerBoundType;
+    public DynamicTypeWithUpperBound getDynamicType() {
+      return dynamicType;
     }
 
     public boolean verifyCorrectnessOfValues(Value dest, Value src, AppView<?> appView) {
-      assert !dynamicUpperBoundType.isBottom();
-      assert !dynamicUpperBoundType.isTop();
-      assert dynamicUpperBoundType.lessThanOrEqualUpToNullability(src.getType(), appView);
+      assert !dynamicType.isBottom();
+      assert !dynamicType.isUnknown();
+      assert dynamicType
+          .getDynamicUpperBoundType()
+          .lessThanOrEqualUpToNullability(src.getType(), appView);
       return true;
     }
 
@@ -326,13 +318,12 @@
         return false;
       }
       DynamicTypeAssumption assumption = (DynamicTypeAssumption) other;
-      return dynamicUpperBoundType == assumption.dynamicUpperBoundType
-          && dynamicLowerBoundType == assumption.dynamicLowerBoundType;
+      return dynamicType.equals(assumption.dynamicType);
     }
 
     @Override
     public int hashCode() {
-      return Objects.hash(dynamicUpperBoundType, dynamicLowerBoundType);
+      return dynamicType.hashCode();
     }
   }
 
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 38e86e1..36ec641 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
@@ -132,7 +132,9 @@
       if (receiverLowerBoundType != null) {
         DexType refinedReceiverType =
             TypeAnalysis.getRefinedReceiverType(appViewWithLiveness, this);
-        assert receiverLowerBoundType.getClassType() == refinedReceiverType
+        assert appViewWithLiveness
+                    .appInfo()
+                    .isSubtype(receiverLowerBoundType.getClassType(), refinedReceiverType)
                 || appView.options().testing.allowTypeErrors
                 || receiver.getDynamicUpperBoundType(appViewWithLiveness).isNullType()
                 || receiverLowerBoundType.isBasedOnMissingClass(appViewWithLiveness)
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 27713d8..cc59793 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
@@ -23,6 +23,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 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.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.analysis.value.AbstractValue;
@@ -1093,7 +1094,7 @@
     return type;
   }
 
-  public DynamicType getDynamicType(AppView<AppInfoWithLiveness> appView) {
+  public DynamicTypeWithUpperBound getDynamicType(AppView<AppInfoWithLiveness> appView) {
     return DynamicType.create(appView, this);
   }
 
@@ -1124,7 +1125,12 @@
       // 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 =
-          aliasedValue.definition.asAssume().getDynamicTypeAssumption().getDynamicUpperBoundType();
+          aliasedValue
+              .definition
+              .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;
@@ -1190,6 +1196,7 @@
               .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/FieldOptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java
index c6070c5..b5e3b1e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java
@@ -6,8 +6,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
@@ -19,9 +18,7 @@
 
   void markFieldAsPropagated(DexEncodedField field);
 
-  void markFieldHasDynamicLowerBoundType(DexEncodedField field, ClassTypeElement type);
-
-  void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeElement type);
+  void markFieldHasDynamicType(DexEncodedField field, DynamicType dynamicType);
 
   void markFieldBitsRead(DexEncodedField field, int bitsRead);
 
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 37ce381..047894d 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
@@ -114,7 +114,6 @@
 import com.google.common.base.Suppliers;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -819,20 +818,9 @@
 
     // Assure that no more optimization feedback left after post processing.
     assert feedback.noUpdatesLeft();
-    assert checkLegacySyntheticsAreInBuilder(appView, builder);
     return builder.build();
   }
 
-  private boolean checkLegacySyntheticsAreInBuilder(
-      AppView<AppInfoWithLiveness> appView, Builder<?> builder) {
-    Collection<DexProgramClass> inAppInfo =
-        appView.appInfo().getSyntheticItems().getLegacyPendingClasses();
-    Collection<DexProgramClass> inBuilder = builder.getSynthesizedClasses();
-    assert inAppInfo.containsAll(inBuilder);
-    assert inBuilder.containsAll(inAppInfo);
-    return true;
-  }
-
   private void waveStart(ProgramMethodSet wave) {
     onWaveDoneActions = Collections.synchronizedList(new ArrayList<>());
   }
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 dafd559..5794f8d 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
@@ -479,7 +479,8 @@
               InstancePut instancePut = current.asInstancePut();
               DexField field = instancePut.getField();
               FieldLookupResult lookup = graphLens.lookupFieldResult(field);
-              insertCastForFieldAssignmentIfNeeded(code, blocks, iterator, instancePut, lookup);
+              iterator =
+                  insertCastForFieldAssignmentIfNeeded(code, blocks, iterator, instancePut, lookup);
 
               DexField rewrittenField = rewriteFieldReference(lookup, method);
               DexMethod replacementMethod =
@@ -550,7 +551,8 @@
               StaticPut staticPut = current.asStaticPut();
               DexField field = staticPut.getField();
               FieldLookupResult lookup = graphLens.lookupFieldResult(field);
-              insertCastForFieldAssignmentIfNeeded(code, blocks, iterator, staticPut, lookup);
+              iterator =
+                  insertCastForFieldAssignmentIfNeeded(code, blocks, iterator, staticPut, lookup);
 
               DexField actualField = rewriteFieldReference(lookup, method);
               DexMethod replacementMethod =
@@ -752,7 +754,7 @@
     assert code.hasNoMergedClasses(appView);
   }
 
-  private void insertCastForFieldAssignmentIfNeeded(
+  private InstructionListIterator insertCastForFieldAssignmentIfNeeded(
       IRCode code,
       ListIterator<BasicBlock> blocks,
       InstructionListIterator iterator,
@@ -774,6 +776,7 @@
       Instruction next = iterator.next();
       assert next == fieldPut;
     }
+    return iterator;
   }
 
   private DexField rewriteFieldReference(FieldLookupResult lookup, ProgramMethod context) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
index 6cf694a..8b229f9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
@@ -9,8 +9,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
@@ -38,10 +37,12 @@
   void methodReturnsAbstractValue(
       DexEncodedMethod method, AppView<AppInfoWithLiveness> appView, AbstractValue abstractValue);
 
-  void methodReturnsObjectWithUpperBoundType(
-      DexEncodedMethod method, AppView<?> appView, TypeElement type);
+  default void setDynamicReturnType(
+      ProgramMethod method, AppView<?> appView, DynamicType dynamicType) {
+    setDynamicReturnType(method.getDefinition(), appView, dynamicType);
+  }
 
-  void methodReturnsObjectWithLowerBoundType(DexEncodedMethod method, ClassTypeElement type);
+  void setDynamicReturnType(DexEncodedMethod method, AppView<?> appView, DynamicType dynamicType);
 
   void methodMayNotHaveSideEffects(DexEncodedMethod method);
 
@@ -92,9 +93,7 @@
 
   void unsetClassInlinerMethodConstraint(ProgramMethod method);
 
-  void unsetDynamicLowerBoundReturnType(ProgramMethod method);
-
-  void unsetDynamicUpperBoundReturnType(ProgramMethod method);
+  void unsetDynamicReturnType(ProgramMethod method);
 
   void unsetEnumUnboxerMethodClassification(ProgramMethod method);
 
@@ -135,8 +134,7 @@
       unsetCheckNullReceiverBeforeAnySideEffect(method);
       unsetClassInitializerMayBePostponed(method);
       unsetClassInlinerMethodConstraint(method);
-      unsetDynamicLowerBoundReturnType(method);
-      unsetDynamicUpperBoundReturnType(method);
+      unsetDynamicReturnType(method);
       unsetEnumUnboxerMethodClassification(method);
       unsetForceInline(method);
       unsetInitializedClassesOnNormalExit(method);
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 efc31d3..30ea9f8 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
@@ -12,7 +12,9 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
-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.DynamicTypeWithUpperBound;
+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.Assume;
@@ -217,10 +219,9 @@
       return false;
     }
 
-    TypeElement dynamicUpperBoundType =
-        TypeElement.fromDexType(invoke.getInvokedMethod().holder, definitelyNotNull(), appView);
-    assumedValuesBuilder.addAssumedValueKnownToDominateAllUsers(
-        invoke, outValue, dynamicUpperBoundType, null);
+    DynamicTypeWithUpperBound dynamicType =
+        invoke.getInvokedMethod().getHolderType().toDynamicType(appView, definitelyNotNull());
+    assumedValuesBuilder.addAssumedValueKnownToDominateAllUsers(invoke, outValue, dynamicType);
     return true;
   }
 
@@ -257,10 +258,7 @@
     if (invoke.hasUsedOutValue()) {
       needsAssumeInstruction =
           computeAssumedValuesForOutValue(
-              invoke,
-              optimizationInfo.getDynamicUpperBoundTypeOrElse(invoke.getOutType()),
-              optimizationInfo.getDynamicLowerBoundType(),
-              assumedValuesBuilder);
+              invoke, optimizationInfo.getDynamicType(), assumedValuesBuilder);
     }
 
     // Case (3), parameters that are not null after the invocation.
@@ -306,35 +304,38 @@
 
     FieldOptimizationInfo optimizationInfo = field.getDefinition().getOptimizationInfo();
     return computeAssumedValuesForOutValue(
-        fieldGet,
-        optimizationInfo.getDynamicUpperBoundTypeOrElse(fieldGet.getOutType()),
-        optimizationInfo.getDynamicLowerBoundType(),
-        assumedValuesBuilder);
+        fieldGet, optimizationInfo.getDynamicType(), assumedValuesBuilder);
   }
 
   private boolean computeAssumedValuesForOutValue(
       Instruction instruction,
-      TypeElement dynamicUpperBoundType,
-      ClassTypeElement dynamicLowerBoundType,
+      DynamicType dynamicType,
       AssumedValues.Builder assumedValuesBuilder) {
     Value outValue = instruction.outValue();
-
     // Do not insert dynamic type information if it does not refine the static type.
-    boolean isRedundant =
-        !dynamicUpperBoundType.strictlyLessThan(outValue.getType(), appView)
-            && dynamicLowerBoundType == null;
-    if (isRedundant) {
+    if (dynamicType.isUnknown()) {
       return false;
     }
 
-    // Do not insert dynamic type information if the dynamic type only refines the nullability.
-    if (dynamicUpperBoundType.equalUpToNullability(outValue.getType())
-        && dynamicLowerBoundType == null) {
-      assert dynamicUpperBoundType.isDefinitelyNotNull();
+    // Insert an assume-not-null instruction if the dynamic type only refines the nullability.
+    if (dynamicType.isNotNullType()) {
+      assumedValuesBuilder.addNonNullValueKnownToDominateAllUsers(instruction, outValue);
+      return true;
+    }
+
+    DynamicTypeWithUpperBound dynamicTypeWithUpperBound = dynamicType.asDynamicTypeWithUpperBound();
+    DynamicTypeWithUpperBound staticType = DynamicType.create(appView, outValue.getType());
+    if (!dynamicTypeWithUpperBound.strictlyLessThan(staticType, appView)) {
+      return false;
+    }
+
+    if (!dynamicTypeWithUpperBound.getNullability().isMaybeNull()
+        && dynamicTypeWithUpperBound.withNullability(Nullability.maybeNull()).equals(staticType)) {
+      assert dynamicTypeWithUpperBound.getNullability().isDefinitelyNotNull();
       assumedValuesBuilder.addNonNullValueKnownToDominateAllUsers(instruction, outValue);
     } else {
       assumedValuesBuilder.addAssumedValueKnownToDominateAllUsers(
-          instruction, outValue, dynamicUpperBoundType, dynamicLowerBoundType);
+          instruction, outValue, dynamicTypeWithUpperBound);
     }
     return true;
   }
@@ -707,21 +708,17 @@
       this.dynamicTypeAssumption = dynamicTypeAssumption;
     }
 
-    void setDynamicTypeAssumption(
-        TypeElement dynamicUpperBoundType, ClassTypeElement dynamicLowerBoundType) {
-      dynamicTypeAssumption =
-          new DynamicTypeAssumption(dynamicUpperBoundType, dynamicLowerBoundType);
-      if (dynamicUpperBoundType.isDefinitelyNotNull()) {
-        setNotNull();
-      }
-      if (dynamicLowerBoundType != null && dynamicLowerBoundType.isDefinitelyNotNull()) {
+    void setDynamicTypeAssumption(DynamicTypeWithUpperBound dynamicType) {
+      assert dynamicType != null;
+      dynamicTypeAssumption = new DynamicTypeAssumption(dynamicType);
+      if (dynamicType.getDynamicUpperBoundType().isDefinitelyNotNull()) {
         setNotNull();
       }
     }
 
     boolean isNull() {
       return dynamicTypeAssumption != null
-          && dynamicTypeAssumption.getDynamicUpperBoundType().isDefinitelyNull();
+          && dynamicTypeAssumption.getDynamicType().getNullability().isDefinitelyNull();
     }
 
     boolean isNonNull() {
@@ -874,17 +871,12 @@
       }
 
       void addAssumedValueKnownToDominateAllUsers(
-          Instruction instruction,
-          Value assumedValue,
-          TypeElement dynamicUpperBoundType,
-          ClassTypeElement dynamicLowerBoundType) {
+          Instruction instruction, Value assumedValue, DynamicTypeWithUpperBound dynamicType) {
         updateAssumedValueInfo(
             instruction,
             assumedValue,
             AssumedDominance.everything(),
-            assumedValueInfo ->
-                assumedValueInfo.setDynamicTypeAssumption(
-                    dynamicUpperBoundType, dynamicLowerBoundType));
+            assumedValueInfo -> assumedValueInfo.setDynamicTypeAssumption(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 85ac5b8..70057fc 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
@@ -33,14 +33,19 @@
   private final AppView<?> appView;
   private final IRCode code;
 
-  private final Set<Value> affectedValues = Sets.newIdentityHashSet();
+  private final Set<Value> affectedValues;
   private final Set<Assume> assumeInstructionsToRemove = Sets.newIdentityHashSet();
 
   private boolean mayHaveIntroducedTrivialPhi = false;
 
   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() {
@@ -57,7 +62,7 @@
         Assume assumeInstruction = user.asAssume();
         assumeInstruction.unsetDynamicTypeAssumption();
         if (!assumeInstruction.hasNonNullAssumption()) {
-          assumeInstruction.unsetDynamicTypeAssumption();
+          markForRemoval(assumeInstruction);
         }
       }
     }
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 dae6f04..a10c4d7 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
@@ -30,6 +30,8 @@
 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.DynamicTypeWithUpperBound;
+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.analysis.value.AbstractValue;
@@ -1655,16 +1657,13 @@
                     value.isDefinedByInstructionSatisfying(
                         Instruction::isAssumeWithDynamicTypeAssumption));
         if (aliasedValue != null) {
-          TypeElement dynamicType =
-              aliasedValue
-                  .definition
-                  .asAssume()
-                  .getDynamicTypeAssumption()
-                  .getDynamicUpperBoundType();
-          if (dynamicType.isDefinitelyNull()) {
+          DynamicTypeWithUpperBound dynamicType =
+              aliasedValue.getDefinition().asAssume().getDynamicTypeAssumption().getDynamicType();
+          Nullability nullability = dynamicType.getNullability();
+          if (nullability.isDefinitelyNull()) {
             result = InstanceOfResult.FALSE;
-          } else if (dynamicType.lessThanOrEqual(instanceOfType, appView)
-              && (!inType.isNullable() || !dynamicType.isNullable())) {
+          } else if (dynamicType.getDynamicUpperBoundType().lessThanOrEqual(instanceOfType, appView)
+              && (!inType.isNullable() || !nullability.isNullable())) {
             result = InstanceOfResult.TRUE;
           }
         }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 8007b17..d612c5f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -161,7 +161,8 @@
 
     SyntheticItems syntheticItems = appView.getSyntheticItems();
     ClassToFeatureSplitMap classToFeatureSplitMap = appView.appInfo().getClassToFeatureSplitMap();
-    if (!classToFeatureSplitMap.isInSameFeatureOrBothInBase(singleTarget, method, syntheticItems)) {
+    if (!classToFeatureSplitMap.isInSameFeatureOrBothInSameBase(
+        singleTarget, method, syntheticItems)) {
       // Still allow inlining if we inline from the base into a feature.
       if (!classToFeatureSplitMap.isInBase(singleTarget.getHolder(), syntheticItems)) {
         whyAreYouNotInliningReporter.reportInliningAcrossFeatureSplit();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
index 9b738b6..10988dc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
@@ -5,9 +5,8 @@
 package com.android.tools.r8.ir.optimize;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.JumpInstruction;
@@ -29,41 +28,16 @@
    *
    * <p>If the method has no normal exits, then null is returned.
    */
-  public TypeElement computeDynamicReturnType(DexEncodedMethod method, IRCode code) {
-    assert method.getReference().proto.returnType.isReferenceType();
-    List<TypeElement> returnedTypes = new ArrayList<>();
-    for (BasicBlock block : code.blocks) {
+  public DynamicType computeDynamicReturnType(ProgramMethod method, IRCode code) {
+    assert method.getReturnType().isReferenceType();
+    List<DynamicType> returnedTypes = new ArrayList<>();
+    for (BasicBlock block : code.getBlocks()) {
       JumpInstruction exitInstruction = block.exit();
       if (exitInstruction.isReturn()) {
         Value returnValue = exitInstruction.asReturn().returnValue();
-        returnedTypes.add(returnValue.getDynamicUpperBoundType(appView));
+        returnedTypes.add(returnValue.getDynamicType(appView));
       }
     }
-    return returnedTypes.isEmpty() ? null : TypeElement.join(returnedTypes, appView);
-  }
-
-  public ClassTypeElement computeDynamicLowerBoundType(DexEncodedMethod method, IRCode code) {
-    assert method.getReference().proto.returnType.isReferenceType();
-    ClassTypeElement result = null;
-    for (BasicBlock block : code.blocks) {
-      JumpInstruction exitInstruction = block.exit();
-      if (exitInstruction.isReturn()) {
-        Value returnValue = exitInstruction.asReturn().returnValue();
-        ClassTypeElement dynamicLowerBoundType = returnValue.getDynamicLowerBoundType(appView);
-        if (dynamicLowerBoundType == null) {
-          return null;
-        }
-        if (result == null) {
-          result = dynamicLowerBoundType;
-        } else if (dynamicLowerBoundType.equalUpToNullability(result)) {
-          if (dynamicLowerBoundType.nullability() != result.nullability()) {
-            result = dynamicLowerBoundType.join(result, appView).asClassType();
-          }
-        } else {
-          return null;
-        }
-      }
-    }
-    return result;
+    return DynamicType.join(appView, returnedTypes);
   }
 }
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 510331b..b4a067e 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
@@ -1108,8 +1108,7 @@
       }
     }
     assert inlineeStack.isEmpty();
-    assumeRemover.removeMarkedInstructions(blocksToRemove);
-    assumeRemover.finish();
+    assumeRemover.removeMarkedInstructions(blocksToRemove).finish();
     classInitializationAnalysis.finish();
     code.removeBlocks(blocksToRemove);
     code.removeAllDeadAndTrivialPhis();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index 206c455..755f334 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -411,7 +411,7 @@
       // Verify that the optimization info is consistent with the static value.
       assert definition.getOptimizationInfo().getAbstractValue().isUnknown()
           || !definition.hasExplicitStaticValue()
-          || abstractValue == definition.getOptimizationInfo().getAbstractValue();
+          || abstractValue.equals(definition.getOptimizationInfo().getAbstractValue());
     } else {
       // This is guaranteed to read the default value of the field.
       abstractValue = appView.abstractValueFactory().createSingleNumberValue(0);
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 afb7003..b3f74c5 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
@@ -16,7 +16,6 @@
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
-import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.value.SingleFieldValue;
 import com.android.tools.r8.ir.analysis.value.SingleValue;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
@@ -189,6 +188,7 @@
       }
     }
 
+    AssumeRemover assumeRemover = new AssumeRemover(appView, code, affectedValues);
     for (BasicBlock head : code.topologicallySortedBlocks()) {
       if (head.hasUniquePredecessor() && head.getUniquePredecessor().hasUniqueNormalSuccessor()) {
         // Already visited.
@@ -211,14 +211,16 @@
             }
 
             if (instruction.isInstanceGet()) {
-              handleInstanceGet(it, instruction.asInstanceGet(), field);
+              handleInstanceGet(it, instruction.asInstanceGet(), field, assumeRemover);
             } else if (instruction.isInstancePut()) {
               handleInstancePut(instruction.asInstancePut(), field);
             } else if (instruction.isStaticGet()) {
-              handleStaticGet(it, instruction.asStaticGet(), field);
+              handleStaticGet(it, instruction.asStaticGet(), field, assumeRemover);
             } 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()) {
@@ -288,9 +290,7 @@
       activeStates.recordActiveStateOnBlockExit(end, activeState);
     }
     processInstructionsToRemove();
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
-    }
+    assumeRemover.removeMarkedInstructions().finish();
     assert code.isConsistentSSA();
   }
 
@@ -398,7 +398,10 @@
   }
 
   private void handleInstanceGet(
-      InstructionListIterator it, InstanceGet instanceGet, DexClassAndField field) {
+      InstructionListIterator it,
+      InstanceGet instanceGet,
+      DexClassAndField field,
+      AssumeRemover assumeRemover) {
     if (instanceGet.outValue().hasLocalInfo()) {
       clearMostRecentInstanceFieldWrite(instanceGet, field);
       return;
@@ -408,6 +411,7 @@
     FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
     FieldValue replacement = activeState.getInstanceFieldValue(fieldAndObject);
     if (replacement != null) {
+      assumeRemover.markAssumeDynamicTypeUsersForRemoval(instanceGet.outValue());
       replacement.eliminateRedundantRead(it, instanceGet);
       return;
     }
@@ -464,7 +468,10 @@
   }
 
   private void handleStaticGet(
-      InstructionListIterator instructionIterator, StaticGet staticGet, DexClassAndField field) {
+      InstructionListIterator instructionIterator,
+      StaticGet staticGet,
+      DexClassAndField field,
+      AssumeRemover assumeRemover) {
     if (staticGet.outValue().hasLocalInfo()) {
       killNonFinalActiveFields(staticGet);
       clearMostRecentStaticFieldWrite(staticGet, field);
@@ -473,6 +480,7 @@
 
     FieldValue replacement = activeState.getStaticFieldValue(field.getReference());
     if (replacement != null) {
+      assumeRemover.markAssumeDynamicTypeUsersForRemoval(staticGet.outValue());
       replacement.eliminateRedundantRead(instructionIterator, staticGet);
       return;
     }
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 c3c2307..442cb96 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
@@ -14,12 +14,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.TypeAnalysis;
 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.optimize.AssumeRemover;
 import com.android.tools.r8.ir.optimize.CodeRewriter;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.InliningOracle;
@@ -209,8 +209,10 @@
 
         // Inline the class instance.
         Set<Value> affectedValues = Sets.newIdentityHashSet();
+        AssumeRemover assumeRemover = new AssumeRemover(appView, code, affectedValues);
         try {
-          anyInlinedMethods |= processor.processInlining(code, affectedValues, inliningIRProvider);
+          anyInlinedMethods |=
+              processor.processInlining(code, affectedValues, assumeRemover, 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.
@@ -224,10 +226,9 @@
         }
 
         // Restore normality.
+        assumeRemover.removeMarkedInstructions();
         code.removeAllDeadAndTrivialPhis(affectedValues);
-        if (!affectedValues.isEmpty()) {
-          new TypeAnalysis(appView).narrowing(affectedValues);
-        }
+        assumeRemover.finish();
         assert code.isConsistentSSA();
         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 82322c7..aede4f7 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
@@ -25,6 +25,7 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 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;
@@ -48,6 +49,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.Inliner;
 import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
@@ -178,13 +180,12 @@
     }
     DexEncodedField field = fieldResolutionResult.getResolvedField();
     FieldOptimizationInfo optimizationInfo = field.getOptimizationInfo();
-    ClassTypeElement dynamicLowerBoundType = optimizationInfo.getDynamicLowerBoundType();
-    if (dynamicLowerBoundType == null
-        || !dynamicLowerBoundType.equals(optimizationInfo.getDynamicUpperBoundType())) {
+    DynamicType dynamicType = optimizationInfo.getDynamicType();
+    if (!dynamicType.isExactClassType()) {
       return EligibilityStatus.NOT_ELIGIBLE;
     }
     eligibleClass =
-        asProgramClassOrNull(appView.definitionFor(dynamicLowerBoundType.getClassType()));
+        asProgramClassOrNull(appView.definitionFor(dynamicType.getExactClassType().getClassType()));
     if (eligibleClass == null) {
       return EligibilityStatus.NOT_ELIGIBLE;
     }
@@ -376,6 +377,7 @@
   boolean processInlining(
       IRCode code,
       Set<Value> affectedValues,
+      AssumeRemover assumeRemover,
       InliningIRProvider inliningIRProvider)
       throws IllegalClassInlinerStateException {
     // Verify that `eligibleInstance` is not aliased.
@@ -386,7 +388,7 @@
 
     rebindIndirectEligibleInstanceUsersFromPhis();
     removeMiscUsages(code, affectedValues);
-    removeFieldReads(code);
+    removeFieldReads(code, assumeRemover);
     removeFieldWrites();
     removeInstruction(root);
     return anyInlinedMethods;
@@ -666,24 +668,26 @@
   }
 
   // Replace field reads with appropriate values, insert phis when needed.
-  private void removeFieldReads(IRCode code) {
+  private void removeFieldReads(IRCode code, AssumeRemover assumeRemover) {
     Set<Value> affectedValues = Sets.newIdentityHashSet();
     if (root.isNewInstance()) {
-      removeFieldReadsFromNewInstance(code, affectedValues);
+      removeFieldReadsFromNewInstance(code, affectedValues, assumeRemover);
     } else {
       assert root.isStaticGet();
-      removeFieldReadsFromStaticGet(code, affectedValues);
+      removeFieldReadsFromStaticGet(code, affectedValues, assumeRemover);
     }
     if (!affectedValues.isEmpty()) {
       new TypeAnalysis(appView).narrowing(affectedValues);
     }
   }
 
-  private void removeFieldReadsFromNewInstance(IRCode code, Set<Value> affectedValues) {
+  private void removeFieldReadsFromNewInstance(
+      IRCode code, Set<Value> affectedValues, AssumeRemover assumeRemover) {
     TreeSet<InstanceGet> uniqueInstanceGetUsersWithDeterministicOrder =
         new TreeSet<>(Comparator.comparingInt(x -> x.outValue().getNumber()));
     for (Instruction user : eligibleInstance.uniqueUsers()) {
       if (user.isInstanceGet()) {
+        assumeRemover.markAssumeDynamicTypeUsersForRemoval(user.outValue());
         if (user.hasUsedOutValue()) {
           uniqueInstanceGetUsersWithDeterministicOrder.add(user.asInstanceGet());
         } else {
@@ -738,7 +742,8 @@
     removeInstruction(fieldRead);
   }
 
-  private void removeFieldReadsFromStaticGet(IRCode code, Set<Value> affectedValues) {
+  private void removeFieldReadsFromStaticGet(
+      IRCode code, Set<Value> affectedValues, AssumeRemover assumeRemover) {
     Set<BasicBlock> seen = Sets.newIdentityHashSet();
     Set<Instruction> users = eligibleInstance.uniqueUsers();
     for (Instruction user : users) {
@@ -759,6 +764,7 @@
         }
 
         if (instruction.isInstanceGet()) {
+          assumeRemover.markAssumeDynamicTypeUsersForRemoval(instruction.outValue());
           if (instruction.hasUsedOutValue()) {
             replaceFieldReadFromStaticGet(
                 code, instructionIterator, user.asInstanceGet(), affectedValues);
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 ff79200..95a4b63 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
@@ -140,7 +140,7 @@
 
         // Since the value is a single field value, the type should be exact.
         assert abstractValue.isSingleFieldValue();
-        ClassTypeElement enumFieldType = optimizationInfo.getExactClassType(appView);
+        ClassTypeElement enumFieldType = optimizationInfo.getDynamicType().getExactClassType();
         if (enumFieldType == null) {
           assert false : "Expected to have an exact dynamic type for enum instance";
           continue;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/CallSiteOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/CallSiteOptimizationInfo.java
index 3a9c9c0..6356bbf 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/CallSiteOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/CallSiteOptimizationInfo.java
@@ -3,24 +3,17 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize.info;
 
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
 
-// A flat lattice structure:
-//   BOTTOM, TOP, and a lattice element that holds accumulated argument info.
+// A flat lattice structure: TOP and a lattice element that holds accumulated argument info.
 public abstract class CallSiteOptimizationInfo {
 
   public static TopCallSiteOptimizationInfo top() {
     return TopCallSiteOptimizationInfo.getInstance();
   }
 
-  public boolean isTop() {
-    return false;
-  }
-
   public boolean isConcreteCallSiteOptimizationInfo() {
     return false;
   }
@@ -29,33 +22,11 @@
     return null;
   }
 
-  public CallSiteOptimizationInfo join(
-      CallSiteOptimizationInfo other, AppView<?> appView, DexEncodedMethod method) {
-    if (other.isTop()) {
-      return other;
-    }
-    if (isTop()) {
-      return this;
-    }
-    assert isConcreteCallSiteOptimizationInfo() && other.isConcreteCallSiteOptimizationInfo();
-    return asConcreteCallSiteOptimizationInfo()
-        .join(other.asConcreteCallSiteOptimizationInfo(), appView, method);
-  }
-
-  public boolean hasUsefulOptimizationInfo(AppView<?> appView, DexEncodedMethod method) {
-    return false;
-  }
-
-
   // The index exactly matches with in values of invocation, i.e., even including receiver.
-  public TypeElement getDynamicUpperBoundType(int argIndex) {
+  public DynamicType getDynamicType(int argIndex) {
     return null;
   }
 
-  // TODO(b/139246447): dynamic lower bound type?
-
-  // TODO(b/69963623): we need to re-run unused argument removal?
-
   // The index exactly matches with in values of invocation, i.e., even including receiver.
   public AbstractValue getAbstractArgumentValue(int argIndex) {
     return UnknownValue.getInstance();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
index 230b15b..2ee5a6e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
@@ -3,31 +3,25 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize.info;
 
-import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
-import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
 import static com.android.tools.r8.utils.MapUtils.canonicalizeEmptyMap;
 import static java.util.Objects.requireNonNull;
 
 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.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription.ArgumentInfoCollection;
 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.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
-import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
-import java.util.List;
 
 // Accumulated optimization info from call sites.
 public class ConcreteCallSiteOptimizationInfo extends CallSiteOptimizationInfo {
@@ -35,7 +29,7 @@
   // inValues() size == DexMethod.arity + (isStatic ? 0 : 1) // receiver
   // That is, this information takes into account the receiver as well.
   private final int size;
-  private final Int2ReferenceMap<TypeElement> dynamicUpperBoundTypes;
+  private final Int2ReferenceMap<DynamicType> dynamicTypes;
   private final Int2ReferenceMap<AbstractValue> constants;
 
   private ConcreteCallSiteOptimizationInfo(int size) {
@@ -44,29 +38,24 @@
 
   private ConcreteCallSiteOptimizationInfo(
       int size,
-      Int2ReferenceMap<TypeElement> dynamicUpperBoundTypes,
+      Int2ReferenceMap<DynamicType> dynamicTypes,
       Int2ReferenceMap<AbstractValue> constants) {
     assert size > 0;
+    assert constants.values().stream().noneMatch(AbstractValue::isUnknown);
+    assert dynamicTypes.values().stream().noneMatch(DynamicType::isUnknown);
     this.size = size;
-    this.dynamicUpperBoundTypes = requireNonNull(dynamicUpperBoundTypes);
+    this.dynamicTypes = requireNonNull(dynamicTypes);
     this.constants = requireNonNull(constants);
   }
 
   private static CallSiteOptimizationInfo create(
       int size,
-      Int2ReferenceMap<TypeElement> dynamicUpperBoundTypes,
+      Int2ReferenceMap<DynamicType> dynamicTypes,
       Int2ReferenceMap<AbstractValue> constants) {
-    return constants.isEmpty() && dynamicUpperBoundTypes.isEmpty()
+    return constants.isEmpty() && dynamicTypes.isEmpty()
         ? top()
         : new ConcreteCallSiteOptimizationInfo(
-            size, canonicalizeEmptyMap(dynamicUpperBoundTypes), canonicalizeEmptyMap(constants));
-  }
-
-  public CallSiteOptimizationInfo fixupAfterExtraNullParameters(int extraNullParameters) {
-    return extraNullParameters > 0
-        ? new ConcreteCallSiteOptimizationInfo(
-            size + extraNullParameters, dynamicUpperBoundTypes, constants)
-        : this;
+            size, canonicalizeEmptyMap(dynamicTypes), canonicalizeEmptyMap(constants));
   }
 
   public CallSiteOptimizationInfo fixupAfterParametersChanged(
@@ -79,7 +68,7 @@
     if (parameterChanges.isEmpty()) {
       if (prototypeChanges.hasExtraParameters()) {
         return new ConcreteCallSiteOptimizationInfo(
-            size + prototypeChanges.numberOfExtraParameters(), dynamicUpperBoundTypes, constants);
+            size + prototypeChanges.numberOfExtraParameters(), dynamicTypes, constants);
       }
       return this;
     }
@@ -94,7 +83,7 @@
 
     Int2ReferenceMap<AbstractValue> rewrittenConstants =
         new Int2ReferenceArrayMap<>(newSizeAfterParameterRemoval);
-    Int2ReferenceMap<TypeElement> rewrittenDynamicUpperBoundTypes =
+    Int2ReferenceMap<DynamicType> rewrittenDynamicTypes =
         new Int2ReferenceArrayMap<>(newSizeAfterParameterRemoval);
     for (int parameterIndex = 0, rewrittenParameterIndex = 0;
         parameterIndex < size;
@@ -105,107 +94,23 @@
         if (!abstractValue.isUnknown()) {
           rewrittenConstants.put(rewrittenParameterIndex, abstractValue);
         }
-        TypeElement dynamicUpperBoundType = dynamicUpperBoundTypes.get(parameterIndex);
-        if (dynamicUpperBoundType != null) {
-          rewrittenDynamicUpperBoundTypes.put(rewrittenParameterIndex, dynamicUpperBoundType);
+        DynamicType dynamicType = dynamicTypes.get(parameterIndex);
+        if (dynamicType != null) {
+          rewrittenDynamicTypes.put(rewrittenParameterIndex, dynamicType);
         }
         rewrittenParameterIndex++;
       }
     }
     return ConcreteCallSiteOptimizationInfo.create(
         newSizeAfterParameterRemoval + prototypeChanges.numberOfExtraParameters(),
-        rewrittenDynamicUpperBoundTypes,
+        rewrittenDynamicTypes,
         rewrittenConstants);
   }
 
-  CallSiteOptimizationInfo join(
-      ConcreteCallSiteOptimizationInfo other, AppView<?> appView, DexEncodedMethod method) {
-    assert size == other.size;
-    assert size == method.getNumberOfArguments();
-    ConcreteCallSiteOptimizationInfo result = new ConcreteCallSiteOptimizationInfo(size);
-    for (int i = 0; i < result.size; i++) {
-      AbstractValue abstractValue =
-          getAbstractArgumentValue(i)
-              .join(
-                  other.getAbstractArgumentValue(i),
-                  appView.abstractValueFactory(),
-                  method.getArgumentType(i));
-      if (abstractValue.isNonTrivial()) {
-        result.constants.put(i, abstractValue);
-      }
-
-      TypeElement thisUpperBoundType = getDynamicUpperBoundType(i);
-      if (thisUpperBoundType == null) {
-        // This means the corresponding argument is primitive. The counterpart should be too.
-        assert other.getDynamicUpperBoundType(i) == null;
-        continue;
-      }
-      assert thisUpperBoundType.isReferenceType();
-      TypeElement otherUpperBoundType = other.getDynamicUpperBoundType(i);
-      assert otherUpperBoundType != null && otherUpperBoundType.isReferenceType();
-      result.dynamicUpperBoundTypes.put(
-          i, thisUpperBoundType.join(otherUpperBoundType, appView));
-    }
-    if (result.hasUsefulOptimizationInfo(appView, method)) {
-      return result;
-    }
-    // As soon as we know the argument collection so far does not have any useful optimization info,
-    // move to TOP so that further collection can be simply skipped.
-    return top();
-  }
-
-  private TypeElement[] getStaticTypes(AppView<?> appView, DexEncodedMethod method) {
-    int argOffset = method.getFirstNonReceiverArgumentIndex();
-    int size = method.getReference().getArity() + argOffset;
-    TypeElement[] staticTypes = new TypeElement[size];
-    if (!method.isStatic()) {
-      staticTypes[0] =
-          TypeElement.fromDexType(method.getHolderType(), definitelyNotNull(), appView);
-    }
-    for (int i = 0; i < method.getReference().getArity(); i++) {
-      staticTypes[i + argOffset] =
-          TypeElement.fromDexType(method.getParameter(i), maybeNull(), appView);
-    }
-    return staticTypes;
-  }
-
   @Override
-  public boolean hasUsefulOptimizationInfo(AppView<?> appView, DexEncodedMethod method) {
-    TypeElement[] staticTypes = getStaticTypes(appView, method);
-    for (int i = 0; i < size; i++) {
-      AbstractValue abstractValue = getAbstractArgumentValue(i);
-      if (abstractValue.isNonTrivial()) {
-        return true;
-      }
-
-      if (!staticTypes[i].isReferenceType()) {
-        continue;
-      }
-      TypeElement dynamicUpperBoundType = getDynamicUpperBoundType(i);
-      if (dynamicUpperBoundType == null) {
-        continue;
-      }
-      // To avoid the full join of type lattices below, separately check if the nullability of
-      // arguments is improved, and if so, we can eagerly conclude that we've collected useful
-      // call site information for this method.
-      Nullability nullability = dynamicUpperBoundType.nullability();
-      if (nullability.isDefinitelyNull()) {
-        return true;
-      }
-      // TODO(b/139246447): Similar to nullability, if dynamic lower bound type is available,
-      //   we stop here and regard that call sites of this method have useful info.
-      // In general, though, we're looking for (strictly) better dynamic types for arguments.
-      if (dynamicUpperBoundType.strictlyLessThan(staticTypes[i], appView)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  @Override
-  public TypeElement getDynamicUpperBoundType(int argIndex) {
+  public DynamicType getDynamicType(int argIndex) {
     assert 0 <= argIndex && argIndex < size;
-    return dynamicUpperBoundTypes.getOrDefault(argIndex, null);
+    return dynamicTypes.getOrDefault(argIndex, DynamicType.unknown());
   }
 
   @Override
@@ -214,47 +119,6 @@
     return constants.getOrDefault(argIndex, UnknownValue.getInstance());
   }
 
-  public static CallSiteOptimizationInfo fromArguments(
-      AppView<AppInfoWithLiveness> appView,
-      DexMethod invokedMethod,
-      List<Value> arguments,
-      ProgramMethod context) {
-    ConcreteCallSiteOptimizationInfo newCallSiteInfo =
-        new ConcreteCallSiteOptimizationInfo(arguments.size());
-    boolean hasReceiver = arguments.size() > invokedMethod.getArity();
-    boolean isTop = true;
-    for (int i = 0; i < newCallSiteInfo.size; i++) {
-      Value arg = arguments.get(i);
-
-      // Constant propagation.
-        Value aliasedValue = arg.getAliasedValue();
-        if (!aliasedValue.isPhi()) {
-          AbstractValue abstractValue = aliasedValue.definition.getAbstractValue(appView, context);
-          if (abstractValue.isNonTrivial()) {
-            newCallSiteInfo.constants.put(i, abstractValue);
-            isTop = false;
-          }
-      }
-
-      // Type propagation.
-      if (arg.getType().isReferenceType()) {
-        TypeElement staticType =
-            TypeElement.fromDexType(
-                hasReceiver ? invokedMethod.holder : invokedMethod.proto.getParameter(i),
-                maybeNull(),
-                appView);
-        TypeElement dynamicUpperBoundType = arg.getDynamicUpperBoundType(appView);
-        if (dynamicUpperBoundType != staticType) {
-          newCallSiteInfo.dynamicUpperBoundTypes.put(i, dynamicUpperBoundType);
-          isTop = false;
-        } else {
-          newCallSiteInfo.dynamicUpperBoundTypes.put(i, staticType);
-        }
-      }
-    }
-    return isTop ? CallSiteOptimizationInfo.top() : newCallSiteInfo;
-  }
-
   public static CallSiteOptimizationInfo fromMethodState(
       AppView<AppInfoWithLiveness> appView,
       ProgramMethod method,
@@ -280,7 +144,7 @@
       // Type propagation.
       DexType staticType = method.getDefinition().getArgumentType(argumentIndex);
       if (staticType.isReferenceType()) {
-        TypeElement staticTypeElement = staticType.toTypeElement(appView);
+        DynamicTypeWithUpperBound staticTypeElement = staticType.toDynamicType(appView);
         if (staticType.isArrayType()) {
           Nullability nullability = concreteParameterState.asArrayParameter().getNullability();
           if (nullability.isDefinitelyNull()) {
@@ -288,8 +152,8 @@
                   argumentIndex, appView.abstractValueFactory().createNullValue());
             isTop = false;
           } else if (nullability.isDefinitelyNotNull()) {
-            newCallSiteInfo.dynamicUpperBoundTypes.put(
-                argumentIndex, staticTypeElement.asArrayType().asDefinitelyNotNull());
+            newCallSiteInfo.dynamicTypes.put(
+                argumentIndex, staticTypeElement.withNullability(Nullability.definitelyNotNull()));
             isTop = false;
           } else {
             // The nullability should never be unknown, since we should use the unknown method state
@@ -300,8 +164,7 @@
         } else if (staticType.isClassType()) {
           DynamicType dynamicType = concreteParameterState.asReferenceParameter().getDynamicType();
           if (!dynamicType.isUnknown()) {
-            newCallSiteInfo.dynamicUpperBoundTypes.put(
-                argumentIndex, dynamicType.getDynamicUpperBoundType());
+            newCallSiteInfo.dynamicTypes.put(argumentIndex, dynamicType);
             isTop = false;
           }
         }
@@ -326,18 +189,17 @@
       return false;
     }
     ConcreteCallSiteOptimizationInfo otherInfo = (ConcreteCallSiteOptimizationInfo) other;
-    return dynamicUpperBoundTypes.equals(otherInfo.dynamicUpperBoundTypes)
-        && constants.equals(otherInfo.constants);
+    return dynamicTypes.equals(otherInfo.dynamicTypes) && constants.equals(otherInfo.constants);
   }
 
   @Override
   public int hashCode() {
-    return System.identityHashCode(dynamicUpperBoundTypes) * 7 + System.identityHashCode(constants);
+    return System.identityHashCode(dynamicTypes) * 7 + System.identityHashCode(constants);
   }
 
   @Override
   public String toString() {
-    return dynamicUpperBoundTypes.toString()
+    return dynamicTypes.toString()
         + (constants == null ? "" : (System.lineSeparator() + constants));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
index c790a02..2e3d1ce 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
@@ -4,8 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.info;
 
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
 
@@ -35,13 +34,8 @@
   }
 
   @Override
-  public ClassTypeElement getDynamicLowerBoundType() {
-    return null;
-  }
-
-  @Override
-  public TypeElement getDynamicUpperBoundType() {
-    return null;
+  public DynamicType getDynamicType() {
+    return DynamicType.unknown();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index 7bc6c1c..1ce6568 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -7,8 +7,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.inlining.NeverSimpleInliningConstraint;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
 import com.android.tools.r8.ir.code.InvokeDirect;
@@ -32,8 +31,6 @@
   static int UNKNOWN_RETURNED_ARGUMENT = -1;
   static boolean UNKNOWN_NEVER_RETURNS_NORMALLY = false;
   static AbstractValue UNKNOWN_ABSTRACT_RETURN_VALUE = UnknownValue.getInstance();
-  static TypeElement UNKNOWN_TYPE = null;
-  static ClassTypeElement UNKNOWN_CLASS_TYPE = null;
   static boolean UNKNOWN_CHECKS_NULL_RECEIVER_BEFORE_ANY_SIDE_EFFECT = false;
   static boolean UNKNOWN_TRIGGERS_CLASS_INIT_BEFORE_ANY_SIDE_EFFECT = false;
   static boolean UNKNOWN_INITIALIZER_ENABLING_JAVA_ASSERTIONS = false;
@@ -74,13 +71,8 @@
   }
 
   @Override
-  public TypeElement getDynamicUpperBoundType() {
-    return UNKNOWN_TYPE;
-  }
-
-  @Override
-  public ClassTypeElement getDynamicLowerBoundType() {
-    return UNKNOWN_CLASS_TYPE;
+  public DynamicType getDynamicType() {
+    return DynamicType.unknown();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
index 8f42838..6493a7d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
@@ -4,13 +4,8 @@
 
 package com.android.tools.r8.ir.optimize.info;
 
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 public abstract class FieldOptimizationInfo
     implements MemberOptimizationInfo<MutableFieldOptimizationInfo> {
@@ -25,32 +20,7 @@
    */
   public abstract int getReadBits();
 
-  public abstract ClassTypeElement getDynamicLowerBoundType();
-
-  public abstract TypeElement getDynamicUpperBoundType();
-
-  public ClassTypeElement getExactClassType(AppView<AppInfoWithLiveness> appView) {
-    ClassTypeElement dynamicLowerBoundType = getDynamicLowerBoundType();
-    TypeElement dynamicUpperBoundType = getDynamicUpperBoundType();
-    if (dynamicUpperBoundType == null || !dynamicUpperBoundType.isClassType()) {
-      return null;
-    }
-    DexType upperType = dynamicUpperBoundType.asClassType().getClassType();
-    if (dynamicLowerBoundType != null && upperType == dynamicLowerBoundType.getClassType()) {
-      return dynamicLowerBoundType;
-    }
-    DexClass upperClass = appView.definitionFor(upperType);
-    if (upperClass != null && upperClass.isEffectivelyFinal(appView)) {
-      assert dynamicLowerBoundType == null;
-      return ClassTypeElement.create(upperType, dynamicUpperBoundType.nullability(), appView);
-    }
-    return null;
-  }
-
-  public final TypeElement getDynamicUpperBoundTypeOrElse(TypeElement orElse) {
-    TypeElement dynamicUpperBoundType = getDynamicUpperBoundType();
-    return dynamicUpperBoundType != null ? dynamicUpperBoundType : orElse;
-  }
+  public abstract DynamicType getDynamicType();
 
   public abstract boolean isDead();
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
index 3de6a7a..b2d7be0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
@@ -6,8 +6,7 @@
 
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
@@ -38,14 +37,7 @@
 
   public abstract EnumUnboxerMethodClassification getEnumUnboxerMethodClassification();
 
-  public abstract TypeElement getDynamicUpperBoundType();
-
-  public final TypeElement getDynamicUpperBoundTypeOrElse(TypeElement orElse) {
-    TypeElement dynamicUpperBoundType = getDynamicUpperBoundType();
-    return dynamicUpperBoundType != null ? dynamicUpperBoundType : orElse;
-  }
-
-  public abstract ClassTypeElement getDynamicLowerBoundType();
+  public abstract DynamicType getDynamicType();
 
   public final boolean hasNonNullParamOrThrow() {
     return getNonNullParamOrThrow() != null;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index d994add..a0c4eb2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -61,9 +61,8 @@
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraintAnalysis;
 import com.android.tools.r8.ir.analysis.sideeffect.ClassInitializerSideEffectAnalysis;
 import com.android.tools.r8.ir.analysis.sideeffect.ClassInitializerSideEffectAnalysis.ClassInitializerSideEffect;
-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.TypeElement;
+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.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.StatefulObjectValue;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
@@ -151,7 +150,7 @@
     }
     computeEnumUnboxerMethodClassification(method, code, feedback, methodProcessor, timing);
     computeSimpleInliningConstraint(method, code, feedback, timing);
-    computeDynamicReturnType(dynamicTypeOptimization, feedback, definition, code, timing);
+    computeDynamicReturnType(dynamicTypeOptimization, feedback, method, code, timing);
     if (options.enableInitializedClassesAnalysis) {
       computeInitializedClassesOnNormalExit(feedback, definition, code, timing);
     }
@@ -874,7 +873,7 @@
   private void computeDynamicReturnType(
       DynamicTypeOptimization dynamicTypeOptimization,
       OptimizationFeedback feedback,
-      DexEncodedMethod method,
+      ProgramMethod method,
       IRCode code,
       Timing timing) {
     timing.begin("Compute dynamic return type");
@@ -885,46 +884,39 @@
   private void computeDynamicReturnType(
       DynamicTypeOptimization dynamicTypeOptimization,
       OptimizationFeedback feedback,
-      DexEncodedMethod method,
+      ProgramMethod method,
       IRCode code) {
-    if (dynamicTypeOptimization != null) {
-      DexType staticReturnTypeRaw = method.getReference().proto.returnType;
-      if (!staticReturnTypeRaw.isReferenceType()) {
-        return;
-      }
-      TypeElement dynamicUpperBoundReturnType =
-          dynamicTypeOptimization.computeDynamicReturnType(method, code);
-      if (dynamicUpperBoundReturnType != null) {
-        if (dynamicUpperBoundReturnType.isReferenceType()
-            && dynamicUpperBoundReturnType.isDefinitelyNull()) {
-          feedback.methodReturnsAbstractValue(
-              method, appView, appView.abstractValueFactory().createSingleNumberValue(0));
-          feedback.methodReturnsObjectWithUpperBoundType(method, appView, TypeElement.getNull());
-        } else {
-          TypeElement staticReturnType =
-              TypeElement.fromDexType(staticReturnTypeRaw, Nullability.maybeNull(), appView);
-          // If the dynamic return type is not more precise than the static return type there is no
-          // need to record it.
-          if (dynamicUpperBoundReturnType.strictlyLessThan(staticReturnType, appView)) {
-            feedback.methodReturnsObjectWithUpperBoundType(
-                method, appView, dynamicUpperBoundReturnType);
-          }
-        }
-      }
+    if (dynamicTypeOptimization == null) {
+      return;
+    }
+    if (!method.getReturnType().isReferenceType()) {
+      return;
+    }
 
-      if (dynamicUpperBoundReturnType != null && dynamicUpperBoundReturnType.isNullType()) {
-        return;
-      }
+    DynamicType dynamicReturnType = dynamicTypeOptimization.computeDynamicReturnType(method, code);
+    if (dynamicReturnType.isBottom() || dynamicReturnType.isUnknown()) {
+      return;
+    }
 
-      ClassTypeElement dynamicLowerBoundReturnType =
-          dynamicTypeOptimization.computeDynamicLowerBoundType(method, code);
-      if (dynamicLowerBoundReturnType != null) {
-        assert dynamicUpperBoundReturnType == null
-            || dynamicUpperBoundReturnType
-                .nullability()
-                .lessThanOrEqual(dynamicLowerBoundReturnType.nullability());
-        feedback.methodReturnsObjectWithLowerBoundType(method, dynamicLowerBoundReturnType);
-      }
+    if (dynamicReturnType.isNullType()) {
+      feedback.methodReturnsAbstractValue(
+          method.getDefinition(), appView, appView.abstractValueFactory().createNullValue());
+      feedback.setDynamicReturnType(method, appView, dynamicReturnType);
+      return;
+    }
+
+    if (dynamicReturnType.isNotNullType()) {
+      feedback.setDynamicReturnType(method, appView, dynamicReturnType);
+      return;
+    }
+
+    // If the dynamic return type is not more precise than the static return type there is no
+    // need to record it.
+    DynamicTypeWithUpperBound staticReturnType = method.getReturnType().toDynamicType(appView);
+    if (dynamicReturnType
+        .asDynamicTypeWithUpperBound()
+        .strictlyLessThan(staticReturnType, appView)) {
+      feedback.setDynamicReturnType(method, appView, dynamicReturnType);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
index 9150ea0..c03d170 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
@@ -6,12 +6,10 @@
 
 import static java.util.Collections.emptySet;
 
-import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -34,38 +32,25 @@
   private AbstractValue abstractValue = UnknownValue.getInstance();
   private int flags;
   private int readBits = 0;
-  private ClassTypeElement dynamicLowerBoundType = null;
-  private TypeElement dynamicUpperBoundType = null;
+  private DynamicType dynamicType = DynamicType.unknown();
 
   public MutableFieldOptimizationInfo fixupClassTypeReferences(
-      AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens lens) {
+      AppView<AppInfoWithLiveness> appView, GraphLens lens) {
     return fixupClassTypeReferences(appView, lens, emptySet());
   }
 
   public MutableFieldOptimizationInfo fixupClassTypeReferences(
-      AppView<? extends AppInfoWithClassHierarchy> appView,
-      GraphLens lens,
-      Set<DexType> prunedTypes) {
-    if (dynamicUpperBoundType != null) {
-      dynamicUpperBoundType = dynamicUpperBoundType.rewrittenWithLens(appView, lens, prunedTypes);
-    }
-    if (dynamicLowerBoundType != null) {
-      TypeElement dynamicLowerBoundType =
-          this.dynamicLowerBoundType.rewrittenWithLens(appView, lens);
-      if (dynamicLowerBoundType.isClassType()) {
-        this.dynamicLowerBoundType = dynamicLowerBoundType.asClassType();
-      } else {
-        assert dynamicLowerBoundType.isPrimitiveType();
-        this.dynamicLowerBoundType = null;
-        this.dynamicUpperBoundType = null;
-      }
-    }
+      AppView<AppInfoWithLiveness> appView, GraphLens lens, Set<DexType> prunedTypes) {
+    dynamicType = dynamicType.rewrittenWithLens(appView, lens, prunedTypes);
     return this;
   }
 
   public MutableFieldOptimizationInfo mutableCopy() {
     MutableFieldOptimizationInfo copy = new MutableFieldOptimizationInfo();
+    copy.abstractValue = abstractValue;
     copy.flags = flags;
+    copy.readBits = readBits;
+    copy.dynamicType = dynamicType;
     return copy;
   }
 
@@ -75,11 +60,12 @@
   }
 
   void setAbstractValue(AbstractValue abstractValue) {
+    assert getAbstractValue().isUnknown() || abstractValue.isNonTrivial();
     this.abstractValue = abstractValue;
   }
 
   public void fixupAbstractValue(AppView<AppInfoWithLiveness> appView, GraphLens lens) {
-    abstractValue = abstractValue.rewrittenWithLens(appView, lens);
+    setAbstractValue(abstractValue.rewrittenWithLens(appView, lens));
   }
 
   @Override
@@ -101,21 +87,12 @@
   }
 
   @Override
-  public ClassTypeElement getDynamicLowerBoundType() {
-    return dynamicLowerBoundType;
+  public DynamicType getDynamicType() {
+    return dynamicType;
   }
 
-  void setDynamicLowerBoundType(ClassTypeElement type) {
-    dynamicLowerBoundType = type;
-  }
-
-  @Override
-  public TypeElement getDynamicUpperBoundType() {
-    return dynamicUpperBoundType;
-  }
-
-  void setDynamicUpperBoundType(TypeElement type) {
-    dynamicUpperBoundType = type;
+  void setDynamicType(DynamicType dynamicType) {
+    this.dynamicType = dynamicType;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 7bda54d..968cc66 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -6,14 +6,14 @@
 
 import static java.util.Collections.emptySet;
 
-import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.ir.analysis.inlining.NeverSimpleInliningConstraint;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
-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.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
@@ -45,9 +45,7 @@
       ClassInlinerMethodConstraint.alwaysFalse();
   private EnumUnboxerMethodClassification enumUnboxerMethodClassification =
       EnumUnboxerMethodClassification.unknown();
-  private TypeElement returnsObjectWithUpperBoundType = DefaultMethodOptimizationInfo.UNKNOWN_TYPE;
-  private ClassTypeElement returnsObjectWithLowerBoundType =
-      DefaultMethodOptimizationInfo.UNKNOWN_CLASS_TYPE;
+  private DynamicType dynamicType = DynamicType.unknown();
   private InlinePreference inlining = InlinePreference.Default;
   // Stores information about instance methods and constructors for
   // class inliner, null value indicates that the method is not eligible.
@@ -150,8 +148,7 @@
     initializedClassesOnNormalExit = template.initializedClassesOnNormalExit;
     returnedArgument = template.returnedArgument;
     abstractReturnValue = template.abstractReturnValue;
-    returnsObjectWithUpperBoundType = template.returnsObjectWithUpperBoundType;
-    returnsObjectWithLowerBoundType = template.returnsObjectWithLowerBoundType;
+    dynamicType = template.dynamicType;
     inlining = template.inlining;
     simpleInliningConstraint = template.simpleInliningConstraint;
     bridgeInfo = template.bridgeInfo;
@@ -177,29 +174,13 @@
   }
 
   public MutableMethodOptimizationInfo fixupClassTypeReferences(
-      AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens lens) {
+      AppView<AppInfoWithLiveness> appView, GraphLens lens) {
     return fixupClassTypeReferences(appView, lens, emptySet());
   }
 
   public MutableMethodOptimizationInfo fixupClassTypeReferences(
-      AppView<? extends AppInfoWithClassHierarchy> appView,
-      GraphLens lens,
-      Set<DexType> prunedTypes) {
-    if (returnsObjectWithUpperBoundType != null) {
-      returnsObjectWithUpperBoundType =
-          returnsObjectWithUpperBoundType.rewrittenWithLens(appView, lens, prunedTypes);
-    }
-    if (returnsObjectWithLowerBoundType != null) {
-      TypeElement returnsObjectWithLowerBoundType =
-          this.returnsObjectWithLowerBoundType.rewrittenWithLens(appView, lens, prunedTypes);
-      if (returnsObjectWithLowerBoundType.isClassType()) {
-        this.returnsObjectWithLowerBoundType = returnsObjectWithLowerBoundType.asClassType();
-      } else {
-        assert returnsObjectWithLowerBoundType.isPrimitiveType();
-        this.returnsObjectWithUpperBoundType = DefaultMethodOptimizationInfo.UNKNOWN_TYPE;
-        this.returnsObjectWithLowerBoundType = DefaultMethodOptimizationInfo.UNKNOWN_CLASS_TYPE;
-      }
-    }
+      AppView<AppInfoWithLiveness> appView, GraphLens lens, Set<DexType> prunedTypes) {
+    dynamicType = dynamicType.rewrittenWithLens(appView, lens, prunedTypes);
     return this;
   }
 
@@ -321,13 +302,8 @@
   }
 
   @Override
-  public TypeElement getDynamicUpperBoundType() {
-    return returnsObjectWithUpperBoundType;
-  }
-
-  @Override
-  public ClassTypeElement getDynamicLowerBoundType() {
-    return returnsObjectWithLowerBoundType;
+  public DynamicType getDynamicType() {
+    return dynamicType;
   }
 
   @Override
@@ -626,8 +602,8 @@
     abstractReturnValue = UnknownValue.getInstance();
   }
 
-  void markReturnsObjectWithUpperBoundType(AppView<?> appView, TypeElement type) {
-    assert type != null;
+  void setDynamicType(AppView<?> appView, DynamicType newDynamicType, DexEncodedMethod method) {
+    assert newDynamicType != null;
     // We may get more precise type information if the method is reprocessed (e.g., due to
     // optimization info collected from all call sites), and hence the
     // `returnsObjectWithUpperBoundType` is allowed to become more precise.
@@ -635,31 +611,30 @@
     // Nullability could be less precise, though. For example, suppose a value is known to be
     // non-null after a safe invocation, hence recorded with the non-null variant. If that call is
     // inlined and the method is reprocessed, such non-null assumption cannot be made again.
-    assert returnsObjectWithUpperBoundType == DefaultMethodOptimizationInfo.UNKNOWN_TYPE
-            || type.lessThanOrEqualUpToNullability(returnsObjectWithUpperBoundType, appView)
-        : "upper bound type changed from " + returnsObjectWithUpperBoundType + " to " + type;
-    returnsObjectWithUpperBoundType = type;
-    if (type.isNullType()) {
-      returnsObjectWithLowerBoundType = null;
+    assert verifyDynamicType(appView, newDynamicType, method);
+    dynamicType = newDynamicType;
+  }
+
+  private boolean verifyDynamicType(
+      AppView<?> appView, DynamicType newDynamicType, DexEncodedMethod method) {
+    if (appView.enableWholeProgramOptimizations()) {
+      TypeElement staticReturnType = method.getReturnType().toTypeElement(appView);
+      TypeElement previousDynamicUpperBoundType =
+          dynamicType.getDynamicUpperBoundType(staticReturnType);
+      TypeElement newDynamicUpperBoundType =
+          newDynamicType.getDynamicUpperBoundType(staticReturnType);
+      assert newDynamicUpperBoundType.lessThanOrEqualUpToNullability(
+              previousDynamicUpperBoundType, appView)
+          : "upper bound type changed from "
+              + previousDynamicUpperBoundType
+              + " to "
+              + newDynamicUpperBoundType;
     }
+    return true;
   }
 
-  void unsetDynamicUpperBoundReturnType() {
-    returnsObjectWithUpperBoundType = null;
-  }
-
-  void markReturnsObjectWithLowerBoundType(ClassTypeElement type) {
-    assert type != null;
-    // Currently, we only have a lower bound type when we have _exact_ runtime type information.
-    // Thus, the type should never become more precise (although the nullability could).
-    assert returnsObjectWithLowerBoundType == DefaultMethodOptimizationInfo.UNKNOWN_CLASS_TYPE
-            || type.equalUpToNullability(returnsObjectWithLowerBoundType)
-        : "lower bound type changed from " + returnsObjectWithLowerBoundType + " to " + type;
-    returnsObjectWithLowerBoundType = type;
-  }
-
-  void unsetDynamicLowerBoundReturnType() {
-    returnsObjectWithLowerBoundType = null;
+  void unsetDynamicType() {
+    dynamicType = DynamicType.unknown();
   }
 
   // TODO(b/140214568): Should be package-private.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
index 23bedd0..5e9386c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
@@ -8,10 +8,10 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
@@ -41,6 +41,10 @@
       new IdentityHashMap<>();
   private final Map<DexEncodedMethod, ConstraintWithTarget> processed = new IdentityHashMap<>();
 
+  private MutableFieldOptimizationInfo getFieldOptimizationInfoForUpdating(ProgramField field) {
+    return getFieldOptimizationInfoForUpdating(field.getDefinition());
+  }
+
   private synchronized MutableFieldOptimizationInfo getFieldOptimizationInfoForUpdating(
       DexEncodedField field) {
     MutableFieldOptimizationInfo info = fieldOptimizationInfos.get(field);
@@ -133,13 +137,8 @@
   }
 
   @Override
-  public void markFieldHasDynamicLowerBoundType(DexEncodedField field, ClassTypeElement type) {
-    getFieldOptimizationInfoForUpdating(field).setDynamicLowerBoundType(type);
-  }
-
-  @Override
-  public void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeElement type) {
-    getFieldOptimizationInfoForUpdating(field).setDynamicUpperBoundType(type);
+  public void markFieldHasDynamicType(DexEncodedField field, DynamicType dynamicType) {
+    getFieldOptimizationInfoForUpdating(field).setDynamicType(dynamicType);
   }
 
   @Override
@@ -199,15 +198,9 @@
   }
 
   @Override
-  public synchronized void methodReturnsObjectWithUpperBoundType(
-      DexEncodedMethod method, AppView<?> appView, TypeElement type) {
-    getMethodOptimizationInfoForUpdating(method).markReturnsObjectWithUpperBoundType(appView, type);
-  }
-
-  @Override
-  public synchronized void methodReturnsObjectWithLowerBoundType(
-      DexEncodedMethod method, ClassTypeElement type) {
-    getMethodOptimizationInfoForUpdating(method).markReturnsObjectWithLowerBoundType(type);
+  public synchronized void setDynamicReturnType(
+      DexEncodedMethod method, AppView<?> appView, DynamicType dynamicType) {
+    getMethodOptimizationInfoForUpdating(method).setDynamicType(appView, dynamicType, method);
   }
 
   @Override
@@ -333,13 +326,8 @@
   }
 
   @Override
-  public synchronized void unsetDynamicLowerBoundReturnType(ProgramMethod method) {
-    getMethodOptimizationInfoForUpdating(method).unsetDynamicLowerBoundReturnType();
-  }
-
-  @Override
-  public synchronized void unsetDynamicUpperBoundReturnType(ProgramMethod method) {
-    getMethodOptimizationInfoForUpdating(method).unsetDynamicUpperBoundReturnType();
+  public synchronized void unsetDynamicReturnType(ProgramMethod method) {
+    getMethodOptimizationInfoForUpdating(method).unsetDynamicType();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
index 3bcd3fc..fd6d27e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
@@ -10,8 +10,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
@@ -44,10 +43,7 @@
   public void markFieldAsPropagated(DexEncodedField field) {}
 
   @Override
-  public void markFieldHasDynamicLowerBoundType(DexEncodedField field, ClassTypeElement type) {}
-
-  @Override
-  public void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeElement type) {}
+  public void markFieldHasDynamicType(DexEncodedField field, DynamicType dynamicType) {}
 
   @Override
   public void markFieldBitsRead(DexEncodedField field, int bitsRead) {}
@@ -79,12 +75,8 @@
       DexEncodedMethod method, AppView<AppInfoWithLiveness> appView, AbstractValue value) {}
 
   @Override
-  public void methodReturnsObjectWithUpperBoundType(
-      DexEncodedMethod method, AppView<?> appView, TypeElement type) {}
-
-  @Override
-  public void methodReturnsObjectWithLowerBoundType(
-      DexEncodedMethod method, ClassTypeElement type) {}
+  public void setDynamicReturnType(
+      DexEncodedMethod method, AppView<?> appView, DynamicType dynamicType) {}
 
   @Override
   public void methodMayNotHaveSideEffects(DexEncodedMethod method) {}
@@ -160,10 +152,7 @@
   public void unsetClassInlinerMethodConstraint(ProgramMethod method) {}
 
   @Override
-  public void unsetDynamicLowerBoundReturnType(ProgramMethod method) {}
-
-  @Override
-  public void unsetDynamicUpperBoundReturnType(ProgramMethod method) {}
+  public void unsetDynamicReturnType(ProgramMethod method) {}
 
   @Override
   public void unsetEnumUnboxerMethodClassification(ProgramMethod method) {}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index 40a6cdd..5fd4800 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -10,8 +10,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
@@ -51,12 +50,7 @@
   }
 
   @Override
-  public void markFieldHasDynamicLowerBoundType(DexEncodedField field, ClassTypeElement type) {
-    // Ignored.
-  }
-
-  @Override
-  public void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeElement type) {
+  public void markFieldHasDynamicType(DexEncodedField field, DynamicType dynamicType) {
     // Ignored.
   }
 
@@ -108,15 +102,9 @@
   }
 
   @Override
-  public void methodReturnsObjectWithUpperBoundType(
-      DexEncodedMethod method, AppView<?> appView, TypeElement type) {
-    method.getMutableOptimizationInfo().markReturnsObjectWithUpperBoundType(appView, type);
-  }
-
-  @Override
-  public void methodReturnsObjectWithLowerBoundType(
-      DexEncodedMethod method, ClassTypeElement type) {
-    // Ignored.
+  public void setDynamicReturnType(
+      DexEncodedMethod method, AppView<?> appView, DynamicType dynamicType) {
+    method.getMutableOptimizationInfo().setDynamicType(appView, dynamicType, method);
   }
 
   @Override
@@ -250,15 +238,8 @@
   }
 
   @Override
-  public void unsetDynamicLowerBoundReturnType(ProgramMethod method) {
-    withMutableMethodOptimizationInfo(
-        method, MutableMethodOptimizationInfo::unsetDynamicLowerBoundReturnType);
-  }
-
-  @Override
-  public void unsetDynamicUpperBoundReturnType(ProgramMethod method) {
-    withMutableMethodOptimizationInfo(
-        method, MutableMethodOptimizationInfo::unsetDynamicUpperBoundReturnType);
+  public void unsetDynamicReturnType(ProgramMethod method) {
+    withMutableMethodOptimizationInfo(method, MutableMethodOptimizationInfo::unsetDynamicType);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/TopCallSiteOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/TopCallSiteOptimizationInfo.java
index 241b934..aa2ef1f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/TopCallSiteOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/TopCallSiteOptimizationInfo.java
@@ -13,9 +13,4 @@
   static TopCallSiteOptimizationInfo getInstance() {
     return INSTANCE;
   }
-
-  @Override
-  public boolean isTop() {
-    return true;
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldInitializationInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldInitializationInfoCollection.java
index ef40285..c5d846e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldInitializationInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldInitializationInfoCollection.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.info.field;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndField;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
@@ -37,6 +38,10 @@
 
   public abstract InstanceFieldInitializationInfo get(DexEncodedField field);
 
+  public final InstanceFieldInitializationInfo get(DexClassAndField field) {
+    return get(field.getDefinition());
+  }
+
   public abstract boolean isEmpty();
 
   public abstract InstanceFieldInitializationInfoCollection fixupAfterParametersChanged(
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 167a528..ee5dc08 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
@@ -10,7 +10,7 @@
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
 import com.android.tools.r8.ir.code.Assume;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Set;
 
 public class EnumMethodOptimizer extends StatelessLibraryMethodModelCollection {
@@ -43,14 +44,18 @@
       DexClassAndMethod singleTarget,
       Set<Value> affectedValues,
       Set<BasicBlock> blocksToRemove) {
-    if (singleTarget.getReference() == appView.dexItemFactory().enumMembers.valueOf
+    if (appView.hasLiveness()
+        && singleTarget.getReference() == appView.dexItemFactory().enumMembers.valueOf
         && invoke.inValues().get(0).isConstClass()) {
-      insertAssumeDynamicType(code, instructionIterator, invoke);
+      insertAssumeDynamicType(appView.withLiveness(), code, instructionIterator, invoke);
     }
   }
 
   private void insertAssumeDynamicType(
-      IRCode code, InstructionListIterator instructionIterator, InvokeMethod invoke) {
+      AppView<AppInfoWithLiveness> appView,
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke) {
     // TODO(b/152516470): Support unboxing enums with Enum#valueOf in try-catch.
     if (invoke.getBlock().hasCatchHandlers()) {
       return;
@@ -62,8 +67,6 @@
         || enumClass.superType != appView.dexItemFactory().enumType) {
       return;
     }
-    TypeElement dynamicUpperBoundType =
-        TypeElement.fromDexType(enumType, definitelyNotNull(), appView);
     Value outValue = invoke.outValue();
     if (outValue == null) {
       return;
@@ -73,9 +76,10 @@
     outValue.replaceUsers(specializedOutValue);
 
     // Insert AssumeDynamicType instruction.
+    DynamicTypeWithUpperBound dynamicType = enumType.toDynamicType(appView, definitelyNotNull());
     Assume assumeInstruction =
         Assume.createAssumeDynamicTypeInstruction(
-            dynamicUpperBoundType, null, specializedOutValue, outValue, invoke, appView);
+            dynamicType, specializedOutValue, outValue, invoke, appView);
     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/LibraryOptimizationInfoInitializer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java
index ca99313..0b41d07 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java
@@ -4,8 +4,6 @@
 
 package com.android.tools.r8.ir.optimize.library;
 
-import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
-
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -14,7 +12,7 @@
 import com.android.tools.r8.graph.DexItemFactory.EnumMembers;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
 import com.android.tools.r8.ir.optimize.info.LibraryOptimizationInfoInitializerFeedback;
@@ -128,16 +126,9 @@
     for (DexMethod method : dexItemFactory.libraryMethodsReturningNonNull) {
       DexEncodedMethod definition = lookupMethod(method);
       if (definition != null) {
-        TypeElement staticType =
-            TypeElement.fromDexType(method.proto.returnType, maybeNull(), appView);
-        feedback.methodReturnsObjectWithUpperBoundType(
-            definition,
-            appView,
-            definition
-                .getOptimizationInfo()
-                .getDynamicUpperBoundTypeOrElse(staticType)
-                .asReferenceType()
-                .asDefinitelyNotNull());
+        assert definition.getOptimizationInfo().getDynamicType().isUnknown()
+            || definition.getOptimizationInfo().getDynamicType().isNotNullType();
+        feedback.setDynamicReturnType(definition, appView, DynamicType.definitelyNotNull());
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
index 9e7d703..bd8696b 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
@@ -54,6 +54,7 @@
   private final int[] metadataVersion;
   private final String inlineClassUnderlyingPropertyName;
   private final KotlinTypeInfo inlineClassUnderlyingType;
+  private final int jvmFlags;
 
   // List of tracked assignments of kotlin metadata.
   private final KotlinMetadataMembersTracker originalMembersWithKotlinInfo;
@@ -77,7 +78,8 @@
       int[] metadataVersion,
       String inlineClassUnderlyingPropertyName,
       KotlinTypeInfo inlineClassUnderlyingType,
-      KotlinMetadataMembersTracker originalMembersWithKotlinInfo) {
+      KotlinMetadataMembersTracker originalMembersWithKotlinInfo,
+      int jvmFlags) {
     this.flags = flags;
     this.name = name;
     this.nameCanBeSynthesizedFromClassOrAnonymousObjectOrigin =
@@ -98,6 +100,7 @@
     this.inlineClassUnderlyingPropertyName = inlineClassUnderlyingPropertyName;
     this.inlineClassUnderlyingType = inlineClassUnderlyingType;
     this.originalMembersWithKotlinInfo = originalMembersWithKotlinInfo;
+    this.jvmFlags = jvmFlags;
   }
 
   public static KotlinClassInfo create(
@@ -180,7 +183,8 @@
         metadataVersion,
         kmClass.getInlineClassUnderlyingPropertyName(),
         KotlinTypeInfo.create(kmClass.getInlineClassUnderlyingType(), factory, reporter),
-        originalMembersWithKotlinInfo);
+        originalMembersWithKotlinInfo,
+        JvmExtensionsKt.getJvmFlags(kmClass));
   }
 
   private static KotlinTypeReference getAnonymousObjectOrigin(
@@ -371,6 +375,7 @@
     }
     JvmClassExtensionVisitor extensionVisitor =
         (JvmClassExtensionVisitor) kmClass.visitExtensions(JvmClassExtensionVisitor.TYPE);
+    extensionVisitor.visitJvmFlags(jvmFlags);
     extensionVisitor.visitModuleName(moduleName);
     if (anonymousObjectOrigin != null) {
       rewritten |=
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataWriter.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataWriter.java
index ee9ea2c..1e4d633 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataWriter.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataWriter.java
@@ -303,6 +303,7 @@
 
   public static void appendKmClass(String indent, StringBuilder sb, KmClass kmClass) {
     appendKeyValue(indent, "flags", sb, kmClass.getFlags() + "");
+    appendKeyValue(indent, "jvmFlags", sb, JvmExtensionsKt.getJvmFlags(kmClass) + "");
     appendKeyValue(indent, "name", sb, kmClass.getName());
     appendKeyValue(
         indent,
@@ -518,13 +519,22 @@
               "setterSignature",
               sb,
               setterSignature != null ? setterSignature.asString() : "null");
-          JvmMethodSignature syntheticMethod =
+          JvmMethodSignature syntheticMethodForAnnotations =
               JvmExtensionsKt.getSyntheticMethodForAnnotations(kmProperty);
           appendKeyValue(
               newIndent,
               "syntheticMethodForAnnotations",
               sb,
-              syntheticMethod != null ? syntheticMethod.asString() : "null");
+              syntheticMethodForAnnotations != null
+                  ? syntheticMethodForAnnotations.asString()
+                  : "null");
+          JvmMethodSignature syntheticMethodForDelegate =
+              JvmExtensionsKt.getSyntheticMethodForAnnotations(kmProperty);
+          appendKeyValue(
+              newIndent,
+              "syntheticMethodForDelegate",
+              sb,
+              syntheticMethodForDelegate != null ? syntheticMethodForDelegate.asString() : "null");
         });
   }
 
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinPropertyInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinPropertyInfo.java
index 9d0597d..39ab90c 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinPropertyInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinPropertyInfo.java
@@ -58,6 +58,8 @@
 
   private final KotlinJvmMethodSignatureInfo syntheticMethodForAnnotations;
 
+  private final KotlinJvmMethodSignatureInfo syntheticMethodForDelegate;
+
   private KotlinPropertyInfo(
       int flags,
       int getterFlags,
@@ -72,7 +74,8 @@
       KotlinJvmFieldSignatureInfo fieldSignature,
       KotlinJvmMethodSignatureInfo getterSignature,
       KotlinJvmMethodSignatureInfo setterSignature,
-      KotlinJvmMethodSignatureInfo syntheticMethodForAnnotations) {
+      KotlinJvmMethodSignatureInfo syntheticMethodForAnnotations,
+      KotlinJvmMethodSignatureInfo syntheticMethodForDelegate) {
     this.flags = flags;
     this.getterFlags = getterFlags;
     this.setterFlags = setterFlags;
@@ -87,6 +90,7 @@
     this.getterSignature = getterSignature;
     this.setterSignature = setterSignature;
     this.syntheticMethodForAnnotations = syntheticMethodForAnnotations;
+    this.syntheticMethodForDelegate = syntheticMethodForDelegate;
   }
 
   public static KotlinPropertyInfo create(
@@ -108,7 +112,9 @@
         KotlinJvmMethodSignatureInfo.create(
             JvmExtensionsKt.getSetterSignature(kmProperty), factory),
         KotlinJvmMethodSignatureInfo.create(
-            JvmExtensionsKt.getSyntheticMethodForAnnotations(kmProperty), factory));
+            JvmExtensionsKt.getSyntheticMethodForAnnotations(kmProperty), factory),
+        KotlinJvmMethodSignatureInfo.create(
+            JvmExtensionsKt.getSyntheticMethodForDelegate(kmProperty), factory));
   }
 
   @Override
@@ -187,6 +193,11 @@
             syntheticMethodForAnnotations.rewrite(
                 extensionVisitor::visitSyntheticMethodForAnnotations, null, appView, namingLens);
       }
+      if (syntheticMethodForDelegate != null) {
+        rewritten |=
+            syntheticMethodForDelegate.rewrite(
+                extensionVisitor::visitSyntheticMethodForDelegate, null, appView, namingLens);
+      }
     }
     return rewritten;
   }
@@ -215,5 +226,8 @@
     if (syntheticMethodForAnnotations != null) {
       syntheticMethodForAnnotations.trace(definitionSupplier);
     }
+    if (syntheticMethodForDelegate != null) {
+      syntheticMethodForDelegate.trace(definitionSupplier);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
index 62bf247..e17606f 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
@@ -137,7 +137,8 @@
     FieldRebindingIdentityLens.Builder builder = FieldRebindingIdentityLens.builder();
     nonReboundFieldReferenceToDefinitionMap.forEach(
         (nonReboundFieldReference, reboundFieldReference) -> {
-          DexField rewrittenReboundFieldReference = lens.lookupField(reboundFieldReference);
+          DexField rewrittenReboundFieldReference =
+              lens.getRenamedFieldSignature(reboundFieldReference);
           DexField rewrittenNonReboundFieldReference =
               rewrittenReboundFieldReference.withHolder(
                   lens.lookupType(nonReboundFieldReference.getHolderType()), dexItemFactory);
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java
index 890cb48..897aaa8 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorApplicationFixer.java
@@ -4,11 +4,24 @@
 
 package com.android.tools.r8.optimize.argumentpropagation;
 
+import static com.android.tools.r8.ir.optimize.info.OptimizationFeedback.getSimpleFeedback;
+
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 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.MethodCollection;
+import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
+import com.android.tools.r8.graph.TreeFixerBase;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfoFixer;
+import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.MutableMethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -20,13 +33,14 @@
  * Takes as input a mapping from old method signatures to new method signatures (with parameters
  * removed), and rewrites all method definitions in the application to their new method signatures.
  */
-public class ArgumentPropagatorApplicationFixer {
+public class ArgumentPropagatorApplicationFixer extends TreeFixerBase {
 
   private final AppView<AppInfoWithLiveness> appView;
   private final ArgumentPropagatorGraphLens graphLens;
 
   public ArgumentPropagatorApplicationFixer(
       AppView<AppInfoWithLiveness> appView, ArgumentPropagatorGraphLens graphLens) {
+    super(appView);
     this.appView = appView;
     this.graphLens = graphLens;
   }
@@ -47,12 +61,25 @@
     ThreadUtils.processItems(affectedClasses, this::fixupClass, executorService);
     timing.end();
 
+    // Fixup optimization info.
+    timing.time("Fixup optimization info", () -> fixupOptimizationInfos(executorService));
+
     timing.begin("Rewrite AppView");
     appView.rewriteWithLens(graphLens);
     timing.end();
   }
 
   private void fixupClass(DexProgramClass clazz) {
+    fixupFields(clazz);
+    fixupMethods(clazz);
+  }
+
+  private void fixupFields(DexProgramClass clazz) {
+    clazz.setInstanceFields(fixupFields(clazz.instanceFields()));
+    clazz.setStaticFields(fixupFields(clazz.staticFields()));
+  }
+
+  private void fixupMethods(DexProgramClass clazz) {
     MethodCollection methodCollection = clazz.getMethodCollection();
     methodCollection.replaceMethods(
         method -> {
@@ -68,11 +95,77 @@
               builder -> {
                 RewrittenPrototypeDescription prototypeChanges =
                     graphLens.getPrototypeChanges(methodReferenceAfterParameterRemoval);
-                builder
-                    .apply(prototypeChanges.createParameterAnnotationsRemover(method))
-                    .fixupOptimizationInfo(
-                        appView, prototypeChanges.createMethodOptimizationInfoFixer());
+                builder.apply(prototypeChanges.createParameterAnnotationsRemover(method));
               });
         });
   }
+
+  private void fixupOptimizationInfos(ExecutorService executorService) throws ExecutionException {
+    PrunedItems prunedItems = PrunedItems.empty(appView.app());
+    getSimpleFeedback()
+        .fixupOptimizationInfos(
+            appView,
+            executorService,
+            new OptimizationInfoFixer() {
+              @Override
+              public void fixup(
+                  DexEncodedField field, MutableFieldOptimizationInfo optimizationInfo) {
+                optimizationInfo.fixupAbstractValue(appView, graphLens);
+              }
+
+              @Override
+              public void fixup(
+                  DexEncodedMethod method, MutableMethodOptimizationInfo optimizationInfo) {
+                // Fixup the return value in case the method returns a field that had its signature
+                // changed.
+                optimizationInfo
+                    .fixupAbstractReturnValue(appView, graphLens)
+                    .fixupInstanceInitializerInfo(appView, graphLens, prunedItems);
+
+                // Rewrite the optimization info to account for method signature changes.
+                if (graphLens.hasPrototypeChanges(method.getReference())) {
+                  RewrittenPrototypeDescription prototypeChanges =
+                      graphLens.getPrototypeChanges(method.getReference());
+                  MethodOptimizationInfoFixer fixer =
+                      prototypeChanges.createMethodOptimizationInfoFixer();
+                  optimizationInfo.fixup(appView, fixer);
+                }
+              }
+            });
+  }
+
+  @Override
+  public DexField fixupFieldReference(DexField field) {
+    return graphLens.internalGetNextFieldSignature(field);
+  }
+
+  @Override
+  public DexMethod fixupMethodReference(DexMethod method) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public DexType fixupType(DexType type) {
+    throw new Unreachable();
+  }
+
+  @Override
+  public DexType mapClassType(DexType type) {
+    return type;
+  }
+
+  @Override
+  public void recordFieldChange(DexField from, DexField to) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void recordMethodChange(DexMethod from, DexMethod to) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void recordClassChange(DexType from, DexType to) {
+    throw new Unreachable();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
index 96e08b6..eef092d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 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.analysis.value.AbstractValue;
@@ -285,11 +286,11 @@
       ProgramMethod resolvedMethod,
       ProgramMethod context,
       ConcretePolymorphicMethodStateOrBottom existingMethodState) {
-    DynamicType dynamicReceiverType = invoke.getReceiver().getDynamicType(appView);
+    DynamicTypeWithUpperBound dynamicReceiverType = invoke.getReceiver().getDynamicType(appView);
     assert !dynamicReceiverType.getDynamicUpperBoundType().nullability().isDefinitelyNull();
 
     ProgramMethod singleTarget = invoke.lookupSingleProgramTarget(appView, context);
-    DynamicType bounds =
+    DynamicTypeWithUpperBound bounds =
         computeBoundsForPolymorphicMethodState(
             invoke, resolvedMethod, singleTarget, context, dynamicReceiverType);
     MethodState existingMethodStateForBounds =
@@ -319,13 +320,13 @@
     return ConcretePolymorphicMethodState.create(bounds, methodStateForBounds);
   }
 
-  private DynamicType computeBoundsForPolymorphicMethodState(
+  private DynamicTypeWithUpperBound computeBoundsForPolymorphicMethodState(
       InvokeMethodWithReceiver invoke,
       ProgramMethod resolvedMethod,
       ProgramMethod singleTarget,
       ProgramMethod context,
-      DynamicType dynamicReceiverType) {
-    DynamicType bounds =
+      DynamicTypeWithUpperBound dynamicReceiverType) {
+    DynamicTypeWithUpperBound bounds =
         singleTarget != null
             ? DynamicType.createExact(
                 singleTarget.getHolderType().toTypeElement(appView).asClassType())
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
index a0f67e0..3ed1334 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorGraphLens.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.optimize.argumentpropagation;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.NestedGraphLens;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
@@ -14,6 +15,7 @@
 import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import java.util.IdentityHashMap;
 import java.util.Map;
+import java.util.Map.Entry;
 
 public class ArgumentPropagatorGraphLens extends NestedGraphLens {
 
@@ -21,9 +23,10 @@
 
   ArgumentPropagatorGraphLens(
       AppView<AppInfoWithLiveness> appView,
+      BidirectionalOneToOneMap<DexField, DexField> fieldMap,
       BidirectionalOneToOneMap<DexMethod, DexMethod> methodMap,
       Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges) {
-    super(appView, EMPTY_FIELD_MAP, methodMap, EMPTY_TYPE_MAP);
+    super(appView, fieldMap, methodMap, EMPTY_TYPE_MAP);
     this.prototypeChanges = prototypeChanges;
   }
 
@@ -31,12 +34,30 @@
     return new Builder(appView);
   }
 
+  public boolean hasPrototypeChanges(DexMethod method) {
+    return method != internalGetPreviousMethodSignature(method);
+  }
+
   public RewrittenPrototypeDescription getPrototypeChanges(DexMethod method) {
-    assert method != internalGetPreviousMethodSignature(method);
+    assert hasPrototypeChanges(method);
     return prototypeChanges.getOrDefault(method, RewrittenPrototypeDescription.none());
   }
 
   @Override
+  protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
+    FieldLookupResult lookupResult = super.internalDescribeLookupField(previous);
+    if (lookupResult.getReference().getType() != previous.getReference().getType()) {
+      return FieldLookupResult.builder(this)
+          .setReboundReference(lookupResult.getReboundReference())
+          .setReference(lookupResult.getReference())
+          .setReadCastType(lookupResult.getReadCastType())
+          .setWriteCastType(lookupResult.getReference().getType())
+          .build();
+    }
+    return lookupResult;
+  }
+
+  @Override
   protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
       RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
     DexMethod previous = internalGetPreviousMethodSignature(method);
@@ -58,6 +79,11 @@
   }
 
   @Override
+  public DexField internalGetNextFieldSignature(DexField field) {
+    return super.internalGetNextFieldSignature(field);
+  }
+
+  @Override
   public DexMethod internalGetNextMethodSignature(DexMethod method) {
     return super.internalGetNextMethodSignature(method);
   }
@@ -65,6 +91,8 @@
   public static class Builder {
 
     private final AppView<AppInfoWithLiveness> appView;
+    private final MutableBidirectionalOneToOneMap<DexField, DexField> newFieldSignatures =
+        new BidirectionalOneToOneHashMap<>();
     private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> newMethodSignatures =
         new BidirectionalOneToOneHashMap<>();
     private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges =
@@ -75,16 +103,23 @@
     }
 
     public boolean isEmpty() {
-      return newMethodSignatures.isEmpty();
+      return newFieldSignatures.isEmpty() && newMethodSignatures.isEmpty();
     }
 
     public ArgumentPropagatorGraphLens.Builder mergeDisjoint(
         ArgumentPropagatorGraphLens.Builder partialGraphLensBuilder) {
+      newFieldSignatures.putAll(partialGraphLensBuilder.newFieldSignatures);
       newMethodSignatures.putAll(partialGraphLensBuilder.newMethodSignatures);
       prototypeChanges.putAll(partialGraphLensBuilder.prototypeChanges);
       return this;
     }
 
+    public Builder recordMove(DexField from, DexField to) {
+      assert from != to;
+      newFieldSignatures.put(from, to);
+      return this;
+    }
+
     public Builder recordMove(
         DexMethod from, DexMethod to, RewrittenPrototypeDescription prototypeChangesForMethod) {
       assert from != to;
@@ -99,9 +134,26 @@
     }
 
     public ArgumentPropagatorGraphLens build() {
-      return isEmpty()
-          ? null
-          : new ArgumentPropagatorGraphLens(appView, newMethodSignatures, prototypeChanges);
+      if (isEmpty()) {
+        return null;
+      }
+      ArgumentPropagatorGraphLens argumentPropagatorGraphLens =
+          new ArgumentPropagatorGraphLens(
+              appView, newFieldSignatures, newMethodSignatures, prototypeChanges);
+      fixupPrototypeChangesAfterFieldSignatureChanges(argumentPropagatorGraphLens);
+      return argumentPropagatorGraphLens;
+    }
+
+    private void fixupPrototypeChangesAfterFieldSignatureChanges(
+        ArgumentPropagatorGraphLens argumentPropagatorGraphLens) {
+      for (Entry<DexMethod, RewrittenPrototypeDescription> entry : prototypeChanges.entrySet()) {
+        RewrittenPrototypeDescription prototypeChangesForMethod = entry.getValue();
+        RewrittenPrototypeDescription rewrittenPrototypeChangesForMethod =
+            prototypeChangesForMethod.rewrittenWithLens(appView, argumentPropagatorGraphLens);
+        if (rewrittenPrototypeChangesForMethod != prototypeChangesForMethod) {
+          entry.setValue(rewrittenPrototypeChangesForMethod);
+        }
+      }
     }
   }
 }
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 892c282..f92af83 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
@@ -5,8 +5,9 @@
 package com.android.tools.r8.optimize.argumentpropagation;
 
 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.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;
@@ -75,16 +76,15 @@
       // TODO(b/190154391): This should also materialize dynamic lower bound information.
       // TODO(b/190154391) This should also materialize the nullability of array arguments.
       if (argumentValue.getType().isReferenceType()) {
-        TypeElement dynamicUpperBoundType =
-            optimizationInfo.getDynamicUpperBoundType(argument.getIndex());
-        if (dynamicUpperBoundType == null || dynamicUpperBoundType.isTop()) {
+        DynamicType dynamicType = optimizationInfo.getDynamicType(argument.getIndex());
+        if (dynamicType.isUnknown()) {
           continue;
         }
-        if (dynamicUpperBoundType.isBottom()) {
+        if (dynamicType.isBottom()) {
           assert false;
           continue;
         }
-        if (dynamicUpperBoundType.isDefinitelyNull()) {
+        if (dynamicType.getNullability().isDefinitelyNull()) {
           ConstNumber nullInstruction = code.createConstNull();
           nullInstruction.setPosition(argument.getPosition());
           affectedValues.addAll(argumentValue.affectedValues());
@@ -92,21 +92,36 @@
           instructionsToAdd.add(nullInstruction);
           continue;
         }
+        if (dynamicType.isNotNullType()) {
+          if (!argumentValue.getType().isDefinitelyNotNull()) {
+            Value nonNullValue =
+                code.createValue(argumentValue.getType().asReferenceType().asMeetWithNotNull());
+            argumentValue.replaceUsers(nonNullValue, affectedValues);
+            Assume assumeNotNull =
+                Assume.createAssumeNonNullInstruction(
+                    nonNullValue, argumentValue, argument, appView);
+            assumeNotNull.setPosition(argument.getPosition());
+            assumeInstructions.add(assumeNotNull);
+          }
+          continue;
+        }
+        DynamicTypeWithUpperBound dynamicTypeWithUpperBound =
+            dynamicType.asDynamicTypeWithUpperBound();
         Value specializedArg;
-        if (dynamicUpperBoundType.strictlyLessThan(argumentValue.getType(), appView)) {
+        if (dynamicTypeWithUpperBound.strictlyLessThan(argumentValue.getType(), appView)) {
           specializedArg = code.createValue(argumentValue.getType());
           affectedValues.addAll(argumentValue.affectedValues());
           argumentValue.replaceUsers(specializedArg);
           Assume assumeType =
               Assume.createAssumeDynamicTypeInstruction(
-                  dynamicUpperBoundType, null, specializedArg, argumentValue, argument, appView);
+                  dynamicTypeWithUpperBound, specializedArg, argumentValue, argument, appView);
           assumeType.setPosition(argument.getPosition());
           assumeInstructions.add(assumeType);
         } else {
           specializedArg = argumentValue;
         }
         assert specializedArg != null && specializedArg.getType().isReferenceType();
-        if (dynamicUpperBoundType.isDefinitelyNotNull()) {
+        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 =
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
index 772a404..f3e9000 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorMethodReprocessingEnqueuer.java
@@ -10,8 +10,10 @@
 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.FieldResolutionResult;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.UseRegistryWithResult;
 import com.android.tools.r8.ir.conversion.IRConverter;
@@ -187,21 +189,43 @@
     }
 
     @Override
+    public void registerInstanceFieldRead(DexField field) {
+      registerFieldAccess(field);
+    }
+
+    @Override
+    public void registerInstanceFieldWrite(DexField field) {
+      registerFieldAccess(field);
+    }
+
+    @Override
+    public void registerStaticFieldRead(DexField field) {
+      registerFieldAccess(field);
+    }
+
+    @Override
+    public void registerStaticFieldWrite(DexField field) {
+      registerFieldAccess(field);
+    }
+
+    private void registerFieldAccess(DexField field) {
+      FieldResolutionResult resolutionResult = appView.appInfo().resolveField(field);
+      if (resolutionResult.getProgramField() == null) {
+        return;
+      }
+
+      ProgramField resolvedField = resolutionResult.getProgramField();
+      DexField rewrittenFieldReference =
+          graphLens.internalGetNextFieldSignature(resolvedField.getReference());
+      if (rewrittenFieldReference != resolvedField.getReference()) {
+        markAffected();
+      }
+    }
+
+    @Override
     public void registerInitClass(DexType type) {}
 
     @Override
-    public void registerInstanceFieldRead(DexField field) {}
-
-    @Override
-    public void registerInstanceFieldWrite(DexField field) {}
-
-    @Override
-    public void registerStaticFieldRead(DexField field) {}
-
-    @Override
-    public void registerStaticFieldWrite(DexField field) {}
-
-    @Override
     public void registerTypeReference(DexType type) {}
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
index 81df33e..94da691 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
@@ -212,8 +212,9 @@
                       .asClassParameter()
                       .getDynamicType();
               DexType staticType = method.getArgumentType(index);
-              assert dynamicType
-                  == WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, staticType);
+              assert dynamicType.isUnknown()
+                  || !WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, staticType)
+                      .isUnknown();
               return true;
             });
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
index 8e7380d..6b94973 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorProgramOptimizer.java
@@ -9,23 +9,32 @@
 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.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription.ArgumentInfoCollection;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription.RemovedArgumentInfo;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription.RewrittenTypeInfo;
+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.DynamicTypeWithUpperBound;
+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.optimize.info.CallSiteOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.ConcreteCallSiteOptimizationInfo;
 import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorGraphLens.Builder;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepFieldInfo;
+import com.android.tools.r8.utils.AccessUtils;
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.IntBox;
@@ -197,10 +206,7 @@
       this.options = appView.options();
     }
 
-    // TODO(b/190154391): Remove unused parameters by simulating they are constant.
     // TODO(b/190154391): Strengthen the static type of parameters.
-    // TODO(b/190154391): If we learn that a method returns a constant, then consider changing its
-    //  return type to void.
     // TODO(b/69963623): If we optimize a method to be unconditionally throwing (because it has a
     //  bottom parameter), then for each caller that becomes unconditionally throwing, we could
     //  also enqueue the caller's callers for reprocessing. This would propagate the throwing
@@ -435,6 +441,15 @@
         DexMethodSignatureSet interfaceDispatchOutsideProgram,
         ArgumentPropagatorGraphLens.Builder partialGraphLensBuilder) {
       BooleanBox affected = new BooleanBox();
+      clazz.forEachProgramFieldMatching(
+          field -> field.getType().isClassType(),
+          field -> {
+            DexField newFieldSignature = getNewFieldSignature(field);
+            if (newFieldSignature != field.getReference()) {
+              partialGraphLensBuilder.recordMove(field.getReference(), newFieldSignature);
+              affected.set();
+            }
+          });
       DexMethodSignatureSet instanceInitializerSignatures = DexMethodSignatureSet.create();
       clazz.forEachProgramInstanceInitializer(instanceInitializerSignatures::add);
       clazz.forEachProgramMethod(
@@ -454,6 +469,71 @@
       return affected.get();
     }
 
+    private DexField getNewFieldSignature(ProgramField field) {
+      DynamicType dynamicType = field.getOptimizationInfo().getDynamicType();
+      if (dynamicType.isUnknown()) {
+        return field.getReference();
+      }
+
+      KeepFieldInfo keepInfo = appView.getKeepInfo(field);
+
+      // We don't have dynamic type information for fields that are kept.
+      assert !keepInfo.isPinned(options);
+
+      if (!keepInfo.isFieldTypeStrengtheningAllowed(options)) {
+        return field.getReference();
+      }
+
+      if (dynamicType.isNullType()) {
+        // Don't optimize always null fields; these will be optimized anyway.
+        return field.getReference();
+      }
+
+      if (dynamicType.isNotNullType()) {
+        // We don't have a more specific type.
+        return field.getReference();
+      }
+
+      DynamicTypeWithUpperBound dynamicTypeWithUpperBound =
+          dynamicType.asDynamicTypeWithUpperBound();
+      TypeElement dynamicUpperBoundType = dynamicTypeWithUpperBound.getDynamicUpperBoundType();
+      assert dynamicUpperBoundType.isReferenceType();
+
+      ClassTypeElement staticFieldType = field.getType().toTypeElement(appView).asClassType();
+      if (dynamicUpperBoundType.equalUpToNullability(staticFieldType)) {
+        // We don't have more precise type information.
+        return field.getReference();
+      }
+
+      if (!dynamicUpperBoundType.strictlyLessThan(staticFieldType, appView)) {
+        assert options.testing.allowTypeErrors;
+        return field.getReference();
+      }
+
+      DexType newStaticFieldType;
+      if (dynamicUpperBoundType.isClassType()) {
+        ClassTypeElement dynamicUpperBoundClassType = dynamicUpperBoundType.asClassType();
+        if (dynamicUpperBoundClassType.getClassType() == dexItemFactory.objectType) {
+          if (dynamicUpperBoundClassType.getInterfaces().hasSingleKnownInterface()) {
+            newStaticFieldType =
+                dynamicUpperBoundClassType.getInterfaces().getSingleKnownInterface();
+          } else {
+            return field.getReference();
+          }
+        } else {
+          newStaticFieldType = dynamicUpperBoundClassType.getClassType();
+        }
+      } else {
+        newStaticFieldType = dynamicUpperBoundType.asArrayType().toDexType(dexItemFactory);
+      }
+
+      if (!AccessUtils.isAccessibleInSameContextsAs(newStaticFieldType, field.getType(), appView)) {
+        return field.getReference();
+      }
+
+      return field.getReference().withType(newStaticFieldType, dexItemFactory);
+    }
+
     private DexMethod getNewMethodSignature(
         ProgramMethod method, RewrittenPrototypeDescription prototypeChanges) {
       DexMethodSignature methodSignatureWithoutPrototypeChanges = method.getMethodSignature();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java
index 9af5fb3..fab9f6f 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java
@@ -67,7 +67,7 @@
 
   @Override
   public Nullability getNullability() {
-    return getDynamicType().getDynamicUpperBoundType().nullability();
+    return getDynamicType().getNullability();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java
index d17ff72..d5fbf2b 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Collection;
 import java.util.HashMap;
@@ -18,24 +19,28 @@
 public class ConcretePolymorphicMethodState extends ConcreteMethodState
     implements ConcretePolymorphicMethodStateOrBottom, ConcretePolymorphicMethodStateOrUnknown {
 
-  private final Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> receiverBoundsToState;
+  private final Map<DynamicTypeWithUpperBound, ConcreteMonomorphicMethodStateOrUnknown>
+      receiverBoundsToState;
 
   private ConcretePolymorphicMethodState(
-      Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> receiverBoundsToState) {
+      Map<DynamicTypeWithUpperBound, ConcreteMonomorphicMethodStateOrUnknown>
+          receiverBoundsToState) {
     this.receiverBoundsToState = receiverBoundsToState;
     assert !isEffectivelyBottom();
     assert !isEffectivelyUnknown();
   }
 
   private ConcretePolymorphicMethodState(
-      DynamicType receiverBounds, ConcreteMonomorphicMethodStateOrUnknown methodState) {
+      DynamicTypeWithUpperBound receiverBounds,
+      ConcreteMonomorphicMethodStateOrUnknown methodState) {
     this.receiverBoundsToState = new HashMap<>(1);
     receiverBoundsToState.put(receiverBounds, methodState);
     assert !isEffectivelyUnknown();
   }
 
   public static ConcretePolymorphicMethodStateOrUnknown create(
-      DynamicType receiverBounds, ConcreteMonomorphicMethodStateOrUnknown methodState) {
+      DynamicTypeWithUpperBound receiverBounds,
+      ConcreteMonomorphicMethodStateOrUnknown methodState) {
     return receiverBounds.isUnknown() && methodState.isUnknown()
         ? MethodState.unknown()
         : new ConcretePolymorphicMethodState(receiverBounds, methodState);
@@ -44,7 +49,7 @@
   private ConcretePolymorphicMethodStateOrUnknown add(
       AppView<AppInfoWithLiveness> appView,
       DexMethodSignature methodSignature,
-      DynamicType bounds,
+      DynamicTypeWithUpperBound bounds,
       ConcreteMonomorphicMethodStateOrUnknown methodState,
       StateCloner cloner) {
     assert !isEffectivelyBottom();
@@ -90,11 +95,12 @@
   }
 
   public void forEach(
-      BiConsumer<? super DynamicType, ? super ConcreteMonomorphicMethodStateOrUnknown> consumer) {
+      BiConsumer<? super DynamicTypeWithUpperBound, ? super ConcreteMonomorphicMethodStateOrUnknown>
+          consumer) {
     receiverBoundsToState.forEach(consumer);
   }
 
-  public MethodState getMethodStateForBounds(DynamicType dynamicType) {
+  public MethodState getMethodStateForBounds(DynamicTypeWithUpperBound dynamicType) {
     ConcreteMonomorphicMethodStateOrUnknown methodStateForBounds =
         receiverBoundsToState.get(dynamicType);
     if (methodStateForBounds != null) {
@@ -115,7 +121,7 @@
   public MethodState mutableCopy() {
     assert !isEffectivelyBottom();
     assert !isEffectivelyUnknown();
-    Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> receiverBoundsToState =
+    Map<DynamicTypeWithUpperBound, ConcreteMonomorphicMethodStateOrUnknown> receiverBoundsToState =
         new HashMap<>();
     forEach((bounds, methodState) -> receiverBoundsToState.put(bounds, methodState.mutableCopy()));
     return new ConcretePolymorphicMethodState(receiverBoundsToState);
@@ -123,16 +129,16 @@
 
   public MethodState mutableCopyWithRewrittenBounds(
       AppView<AppInfoWithLiveness> appView,
-      Function<DynamicType, DynamicType> boundsRewriter,
+      Function<DynamicTypeWithUpperBound, DynamicTypeWithUpperBound> boundsRewriter,
       DexMethodSignature methodSignature,
       StateCloner cloner) {
     assert !isEffectivelyBottom();
     assert !isEffectivelyUnknown();
-    Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> rewrittenReceiverBoundsToState =
-        new HashMap<>();
-    for (Entry<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> entry :
+    Map<DynamicTypeWithUpperBound, ConcreteMonomorphicMethodStateOrUnknown>
+        rewrittenReceiverBoundsToState = new HashMap<>();
+    for (Entry<DynamicTypeWithUpperBound, ConcreteMonomorphicMethodStateOrUnknown> entry :
         receiverBoundsToState.entrySet()) {
-      DynamicType rewrittenBounds = boundsRewriter.apply(entry.getKey());
+      DynamicTypeWithUpperBound rewrittenBounds = boundsRewriter.apply(entry.getKey());
       if (rewrittenBounds == null) {
         continue;
       }
@@ -160,7 +166,7 @@
     assert !isEffectivelyUnknown();
     assert !methodState.isEffectivelyBottom();
     assert !methodState.isEffectivelyUnknown();
-    for (Entry<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> entry :
+    for (Entry<DynamicTypeWithUpperBound, ConcreteMonomorphicMethodStateOrUnknown> entry :
         methodState.receiverBoundsToState.entrySet()) {
       ConcretePolymorphicMethodStateOrUnknown result =
           add(appView, methodSignature, entry.getKey(), entry.getValue(), cloner);
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java
index 74c9107..4da32d6 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java
@@ -54,7 +54,7 @@
 
   @Override
   public Nullability getNullability() {
-    return getDynamicType().getDynamicUpperBoundType().nullability();
+    return getDynamicType().getNullability();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
index 36525f5..085d4a0 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
@@ -16,7 +16,7 @@
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 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.DynamicTypeWithUpperBound;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePolymorphicMethodState;
@@ -46,7 +46,7 @@
 
     // Argument information for virtual methods that is currently inactive, but should be propagated
     // to all overrides below a given upper bound.
-    final Map<DynamicType, MethodStateCollectionBySignature> inactiveUntilUpperBound =
+    final Map<DynamicTypeWithUpperBound, MethodStateCollectionBySignature> inactiveUntilUpperBound =
         new HashMap<>();
 
     PropagationState(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/WideningUtils.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/WideningUtils.java
index f30aa8e..8e657dd 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/WideningUtils.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/WideningUtils.java
@@ -11,7 +11,9 @@
 import com.android.tools.r8.graph.ProgramMethod;
 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.DynamicTypeWithUpperBound;
 import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 public class WideningUtils {
@@ -20,13 +22,11 @@
       AppView<AppInfoWithLiveness> appView,
       ProgramMethod resolvedMethod,
       DynamicType dynamicReceiverType) {
-    return shouldWidenDynamicType(
-            appView,
-            dynamicReceiverType,
-            resolvedMethod.getHolderType(),
-            Nullability.definitelyNotNull())
-        ? DynamicType.unknown()
-        : dynamicReceiverType;
+    return internalWidenDynamicClassType(
+        appView,
+        dynamicReceiverType,
+        resolvedMethod.getHolderType(),
+        Nullability.definitelyNotNull());
   }
 
   public static DynamicType widenDynamicNonReceiverType(
@@ -39,39 +39,38 @@
       DynamicType dynamicType,
       DexType staticType,
       Nullability staticNullability) {
-    return shouldWidenDynamicType(appView, dynamicType, staticType, staticNullability)
-        ? DynamicType.unknown()
-        : dynamicType;
+    return internalWidenDynamicClassType(appView, dynamicType, staticType, staticNullability);
   }
 
-  private static boolean shouldWidenDynamicType(
+  private static DynamicType internalWidenDynamicClassType(
       AppView<AppInfoWithLiveness> appView,
       DynamicType dynamicType,
       DexType staticType,
       Nullability staticNullability) {
     assert staticType.isClassType();
-    if (dynamicType.isUnknown()) {
-      return true;
-    }
     if (dynamicType.isBottom()
         || dynamicType.isNullType()
-        || dynamicType.getNullability().strictlyLessThan(staticNullability)) {
-      return false;
+        || dynamicType.isNotNullType()
+        || dynamicType.isUnknown()) {
+      return dynamicType;
     }
+    DynamicTypeWithUpperBound dynamicTypeWithUpperBound = dynamicType.asDynamicTypeWithUpperBound();
+    TypeElement dynamicUpperBoundType = dynamicTypeWithUpperBound.getDynamicUpperBoundType();
     ClassTypeElement staticTypeElement =
         staticType.toTypeElement(appView).asClassType().getOrCreateVariant(staticNullability);
-    if (!dynamicType.getDynamicUpperBoundType().equals(staticTypeElement)) {
-      return false;
+    if (dynamicType.getNullability().strictlyLessThan(staticNullability)) {
+      if (dynamicType.getNullability().isDefinitelyNotNull()
+          && dynamicUpperBoundType.equalUpToNullability(staticTypeElement)
+          && hasTrivialLowerBound(appView, dynamicType, staticType)) {
+        return DynamicType.definitelyNotNull();
+      }
+      return dynamicType;
+    }
+    if (!dynamicUpperBoundType.equals(staticTypeElement)) {
+      return dynamicType;
     }
     if (!dynamicType.hasDynamicLowerBoundType()) {
-      return true;
-    }
-
-    DexClass staticTypeClass = appView.definitionFor(staticType);
-    if (staticTypeClass == null || !staticTypeClass.isProgramClass()) {
-      // TODO(b/190154391): If this is a library class with no program subtypes, then we might as
-      //  well widen to 'unknown'.
-      return false;
+      return DynamicType.unknown();
     }
 
     // If the static type does not have any program subtypes, then widen the dynamic type to
@@ -80,6 +79,23 @@
     // Note that if the static type is pinned, it could have subtypes outside the set of program
     // classes, but in this case it is still unlikely that we can use the dynamic lower bound type
     // information for anything, so we intentionally also widen to 'unknown' in this case.
+    return isEffectivelyFinal(appView, staticType) ? DynamicType.unknown() : dynamicType;
+  }
+
+  private static boolean hasTrivialLowerBound(
+      AppView<AppInfoWithLiveness> appView, DynamicType dynamicType, DexType staticType) {
+    return !dynamicType.hasDynamicLowerBoundType() || isEffectivelyFinal(appView, staticType);
+  }
+
+  private static boolean isEffectivelyFinal(
+      AppView<AppInfoWithLiveness> appView, DexType staticType) {
+    DexClass staticTypeClass = appView.definitionFor(staticType);
+    if (staticTypeClass == null) {
+      return false;
+    }
+    if (!staticTypeClass.isProgramClass()) {
+      return staticTypeClass.isFinal();
+    }
     ObjectAllocationInfoCollection objectAllocationInfoCollection =
         appView.appInfo().getObjectAllocationInfoCollection();
     return !objectAllocationInfoCollection.hasInstantiatedStrictSubtype(
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 32654be..244257a 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.graph.DexAnnotation.AnnotatedKind;
 import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexDefinition;
 import com.android.tools.r8.graph.DexEncodedAnnotation;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -385,13 +384,6 @@
     }
   }
 
-  public static void clearAnnotations(AppView<?> appView) {
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      clazz.clearAnnotations();
-      clazz.members().forEach(DexDefinition::clearAnnotations);
-    }
-  }
-
   public static class Builder {
 
     /**
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 61c9077..c34f7f3 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndField;
 import com.android.tools.r8.graph.DexClassAndMember;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexClasspathClass;
@@ -978,10 +979,7 @@
   private boolean isInstantiatedDirectly(DexProgramClass clazz) {
     assert checkIfObsolete();
     DexType type = clazz.type;
-    return
-    // TODO(b/165224388): Synthetic classes should be represented in the allocation info.
-    getSyntheticItems().isLegacySyntheticClass(clazz)
-        || (!clazz.isInterface() && objectAllocationInfoCollection.isInstantiatedDirectly(clazz))
+    return (!clazz.isInterface() && objectAllocationInfoCollection.isInstantiatedDirectly(clazz))
         // TODO(b/145344105): Model annotations in the object allocation info.
         || (clazz.isAnnotation() && liveTypes.contains(type));
   }
@@ -1060,6 +1058,10 @@
         && !fieldAccessInfo.isWrittenOutside(method);
   }
 
+  public boolean isInstanceFieldWrittenOnlyInInstanceInitializers(DexClassAndField field) {
+    return isInstanceFieldWrittenOnlyInInstanceInitializers(field.getDefinition());
+  }
+
   public boolean isInstanceFieldWrittenOnlyInInstanceInitializers(DexEncodedField field) {
     assert checkIfObsolete();
     assert isFieldWritten(field) : "Expected field `" + field.toSourceString() + "` to be written";
@@ -1269,7 +1271,7 @@
         getClassToFeatureSplitMap().rewrittenWithLens(lens),
         getMainDexInfo().rewrittenWithLens(getSyntheticItems(), lens),
         deadProtoTypes,
-        getMissingClasses().commitSyntheticItems(committedItems),
+        getMissingClasses(),
         lens.rewriteReferences(liveTypes),
         lens.rewriteReferences(targetedMethods),
         lens.rewriteReferences(failedMethodResolutionTargets),
diff --git a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
index a5b539b..597a07d 100644
--- a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
@@ -181,6 +181,7 @@
 
   @Override
   public void registerExceptionGuard(DexType guard) {
+    setMaxApiReferenceLevel(guard);
     enqueuer.traceExceptionGuard(guard, getContext());
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
index 140b86b..312b61e 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
@@ -74,6 +74,16 @@
       allowFieldTypeStrengthening = original.internalIsFieldTypeStrengtheningAllowed();
     }
 
+    @Override
+    public Builder makeTop() {
+      return super.makeTop().disallowFieldTypeStrengthening();
+    }
+
+    @Override
+    public Builder makeBottom() {
+      return super.makeBottom().allowFieldTypeStrengthening();
+    }
+
     public boolean isFieldTypeStrengtheningAllowed() {
       return allowFieldTypeStrengthening;
     }
@@ -112,6 +122,12 @@
     }
 
     @Override
+    boolean internalIsEqualTo(KeepFieldInfo other) {
+      return super.internalIsEqualTo(other)
+          && isFieldTypeStrengtheningAllowed() == other.internalIsFieldTypeStrengtheningAllowed();
+    }
+
+    @Override
     public KeepFieldInfo doBuild() {
       return new KeepFieldInfo(this);
     }
@@ -136,7 +152,10 @@
     @Override
     public Joiner merge(Joiner joiner) {
       // Should be extended to merge the fields of this class in case any are added.
-      return super.merge(joiner);
+      return super.merge(joiner)
+          .applyIf(
+              !joiner.builder.isFieldTypeStrengtheningAllowed(),
+              KeepFieldInfo.Joiner::disallowFieldTypeStrengthening);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/shaking/MissingClasses.java b/src/main/java/com/android/tools/r8/shaking/MissingClasses.java
index ac8f1fb..5e33712 100644
--- a/src/main/java/com/android/tools/r8/shaking/MissingClasses.java
+++ b/src/main/java/com/android/tools/r8/shaking/MissingClasses.java
@@ -21,7 +21,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramDerivedContext;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.synthesis.CommittedItems;
 import com.android.tools.r8.synthesis.SyntheticItems.SynthesizingContextOracle;
 import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.ImmutableSet;
@@ -51,13 +50,6 @@
     return new MissingClasses(Sets.newIdentityHashSet());
   }
 
-  public MissingClasses commitSyntheticItems(CommittedItems committedItems) {
-    return builder()
-        // TODO(b/175542052): Synthetic types should not be reported as missing in the first place.
-        .removeAlreadyMissingClasses(committedItems.getLegacySyntheticTypes())
-        .build();
-  }
-
   public void forEach(Consumer<DexType> missingClassConsumer) {
     missingClasses.forEach(missingClassConsumer);
   }
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 9822a54..3155b54 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -387,7 +387,7 @@
 
     if (!appInfo
         .getClassToFeatureSplitMap()
-        .isInSameFeatureOrBothInBase(sourceClass, targetClass, appView.getSyntheticItems())) {
+        .isInSameFeatureOrBothInSameBase(sourceClass, targetClass, appView.getSyntheticItems())) {
       return false;
     }
     if (appView.appServices().allServiceTypes().contains(sourceClass.type)
diff --git a/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java b/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
index f71c94e..1e4071f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/CommittedItems.java
@@ -54,11 +54,6 @@
     return committedProgramTypes;
   }
 
-  @Deprecated
-  public Collection<DexType> getLegacySyntheticTypes() {
-    return committed.getLegacyTypes().keySet();
-  }
-
   @Override
   public DexClass definitionFor(DexType type, Function<DexType, DexClass> baseDefinitionFor) {
     // All synthetic types are committed to the application so lookup is just the base lookup.
diff --git a/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
index fd7437e..6961fb0 100644
--- a/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
+++ b/src/main/java/com/android/tools/r8/synthesis/CommittedSyntheticsCollection.java
@@ -34,7 +34,6 @@
     private final CommittedSyntheticsCollection parent;
     private Map<DexType, List<SyntheticProgramClassReference>> newNonLegacyClasses = null;
     private Map<DexType, List<SyntheticMethodReference>> newNonLegacyMethods = null;
-    private Map<DexType, List<LegacySyntheticReference>> newLegacyClasses = null;
     private ImmutableSet.Builder<DexType> newSyntheticInputs = null;
 
     public Builder(CommittedSyntheticsCollection parent) {
@@ -76,28 +75,6 @@
       return this;
     }
 
-    public Builder addLegacyClasses(Map<DexType, LegacySyntheticDefinition> classes) {
-      if (newLegacyClasses == null) {
-        newLegacyClasses = new IdentityHashMap<>();
-      }
-      classes.forEach(
-          (type, item) ->
-              newLegacyClasses
-                  .computeIfAbsent(type, ignore -> new ArrayList<>())
-                  .add(item.toReference()));
-      return this;
-    }
-
-    public Builder addLegacyClass(LegacySyntheticReference reference) {
-      if (newLegacyClasses == null) {
-        newLegacyClasses = new IdentityHashMap<>();
-      }
-      newLegacyClasses
-          .computeIfAbsent(reference.getHolder(), ignore -> new ArrayList<>())
-          .add(reference);
-      return this;
-    }
-
     public Builder addSyntheticInput(DexType syntheticInput) {
       if (newSyntheticInputs == null) {
         newSyntheticInputs = ImmutableSet.builder();
@@ -116,26 +93,21 @@
       if (newNonLegacyMethods != null) {
         newSyntheticInputs.addAll(newNonLegacyMethods.keySet());
       }
-      if (newLegacyClasses != null) {
-        newSyntheticInputs.addAll(newLegacyClasses.keySet());
-      }
       return this;
     }
 
     public CommittedSyntheticsCollection build() {
-      if (newNonLegacyClasses == null && newNonLegacyMethods == null && newLegacyClasses == null) {
+      if (newNonLegacyClasses == null && newNonLegacyMethods == null) {
         return parent;
       }
       ImmutableMap<DexType, List<SyntheticProgramClassReference>> allNonLegacyClasses =
           merge(newNonLegacyClasses, parent.nonLegacyClasses);
       ImmutableMap<DexType, List<SyntheticMethodReference>> allNonLegacyMethods =
           merge(newNonLegacyMethods, parent.nonLegacyMethods);
-      ImmutableMap<DexType, List<LegacySyntheticReference>> allLegacyClasses =
-          merge(newLegacyClasses, parent.legacyTypes);
       ImmutableSet<DexType> allSyntheticInputs =
           newSyntheticInputs == null ? parent.syntheticInputs : newSyntheticInputs.build();
       return new CommittedSyntheticsCollection(
-          allLegacyClasses, allNonLegacyMethods, allNonLegacyClasses, allSyntheticInputs);
+          allNonLegacyMethods, allNonLegacyClasses, allSyntheticInputs);
     }
   }
 
@@ -151,15 +123,7 @@
   }
 
   private static final CommittedSyntheticsCollection EMPTY =
-      new CommittedSyntheticsCollection(
-          ImmutableMap.of(), ImmutableMap.of(), ImmutableMap.of(), ImmutableSet.of());
-
-  /**
-   * Immutable set of synthetic types in the application (eg, committed).
-   *
-   * <p>TODO(b/158159959): Remove legacy support.
-   */
-  private final ImmutableMap<DexType, List<LegacySyntheticReference>> legacyTypes;
+      new CommittedSyntheticsCollection(ImmutableMap.of(), ImmutableMap.of(), ImmutableSet.of());
 
   /** Mapping from synthetic type to its synthetic method item description. */
   private final ImmutableMap<DexType, List<SyntheticMethodReference>> nonLegacyMethods;
@@ -171,11 +135,9 @@
   public final ImmutableSet<DexType> syntheticInputs;
 
   public CommittedSyntheticsCollection(
-      ImmutableMap<DexType, List<LegacySyntheticReference>> legacyTypes,
       ImmutableMap<DexType, List<SyntheticMethodReference>> nonLegacyMethods,
       ImmutableMap<DexType, List<SyntheticProgramClassReference>> nonLegacyClasses,
       ImmutableSet<DexType> syntheticInputs) {
-    this.legacyTypes = legacyTypes;
     this.nonLegacyMethods = nonLegacyMethods;
     this.nonLegacyClasses = nonLegacyClasses;
     this.syntheticInputs = syntheticInputs;
@@ -185,7 +147,6 @@
   private boolean verifySyntheticInputsSubsetOfSynthetics() {
     Set<DexType> synthetics =
         ImmutableSet.<DexType>builder()
-            .addAll(legacyTypes.keySet())
             .addAll(nonLegacyMethods.keySet())
             .addAll(nonLegacyClasses.keySet())
             .build();
@@ -206,14 +167,13 @@
   }
 
   boolean isEmpty() {
-    boolean empty =
-        legacyTypes.isEmpty() && nonLegacyMethods.isEmpty() && nonLegacyClasses.isEmpty();
+    boolean empty = nonLegacyMethods.isEmpty() && nonLegacyClasses.isEmpty();
     assert !empty || syntheticInputs.isEmpty();
     return empty;
   }
 
   boolean containsType(DexType type) {
-    return containsLegacyType(type) || containsNonLegacyType(type);
+    return containsNonLegacyType(type);
   }
 
   boolean containsTypeOfKind(DexType type, SyntheticKind kind) {
@@ -229,10 +189,6 @@
     return false;
   }
 
-  public boolean containsLegacyType(DexType type) {
-    return legacyTypes.containsKey(type);
-  }
-
   public boolean containsNonLegacyType(DexType type) {
     return nonLegacyMethods.containsKey(type) || nonLegacyClasses.containsKey(type);
   }
@@ -241,14 +197,6 @@
     return syntheticInputs.contains(type);
   }
 
-  public ImmutableMap<DexType, List<LegacySyntheticReference>> getLegacyTypes() {
-    return legacyTypes;
-  }
-
-  public List<LegacySyntheticReference> getLegacyTypes(DexType type) {
-    return legacyTypes.getOrDefault(type, emptyList());
-  }
-
   public ImmutableMap<DexType, List<SyntheticMethodReference>> getNonLegacyMethods() {
     return nonLegacyMethods;
   }
@@ -279,13 +227,6 @@
     }
     Builder builder = CommittedSyntheticsCollection.empty().builder();
     boolean changed = false;
-    for (LegacySyntheticReference reference : IterableUtils.flatten(legacyTypes.values())) {
-      if (removed.contains(reference.getHolder())) {
-        changed = true;
-      } else {
-        builder.addLegacyClass(reference);
-      }
-    }
     for (SyntheticMethodReference reference : IterableUtils.flatten(nonLegacyMethods.values())) {
       if (removed.contains(reference.getHolder())) {
         changed = true;
@@ -314,7 +255,6 @@
   CommittedSyntheticsCollection rewriteWithLens(NonIdentityGraphLens lens) {
     ImmutableSet.Builder<DexType> syntheticInputsBuilder = ImmutableSet.builder();
     return new CommittedSyntheticsCollection(
-        rewriteItems(legacyTypes, lens, syntheticInputsBuilder),
         rewriteItems(nonLegacyMethods, lens, syntheticInputsBuilder),
         rewriteItems(nonLegacyClasses, lens, syntheticInputsBuilder),
         syntheticInputsBuilder.build());
@@ -340,7 +280,6 @@
   }
 
   boolean verifyTypesAreInApp(DexApplication application) {
-    assert verifyTypesAreInApp(application, legacyTypes.keySet());
     assert verifyTypesAreInApp(application, nonLegacyMethods.keySet());
     assert verifyTypesAreInApp(application, nonLegacyClasses.keySet());
     assert verifyTypesAreInApp(application, syntheticInputs);
diff --git a/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticDefinition.java b/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticDefinition.java
deleted file mode 100644
index 3e50bf5..0000000
--- a/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticDefinition.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.synthesis;
-
-import com.android.tools.r8.FeatureSplit;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.ProgramDefinition;
-import com.android.tools.r8.utils.IterableUtils;
-import com.google.common.collect.ImmutableSet;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-public class LegacySyntheticDefinition {
-  private final DexProgramClass clazz;
-  private final Map<DexType, FeatureSplit> contexts = new ConcurrentHashMap<>();
-
-  public LegacySyntheticDefinition(DexProgramClass clazz) {
-    this.clazz = clazz;
-  }
-
-  public void addContext(ProgramDefinition clazz, FeatureSplit featureSplit) {
-    DexType type = clazz.getContextType();
-    contexts.put(type, featureSplit);
-  }
-
-  public Set<DexType> getContexts() {
-    return contexts.keySet();
-  }
-
-  public LegacySyntheticReference toReference() {
-    return new LegacySyntheticReference(
-        clazz.getType(), ImmutableSet.copyOf(contexts.keySet()), getFeatureSplit());
-  }
-
-  public FeatureSplit getFeatureSplit() {
-    assert verifyConsistentFeatures();
-    if (contexts.isEmpty()) {
-      return FeatureSplit.BASE;
-    }
-    return IterableUtils.first(contexts.values());
-  }
-
-  private boolean verifyConsistentFeatures() {
-    HashSet<FeatureSplit> features = new HashSet<>(contexts.values());
-    assert features.size() < 2;
-    return true;
-  }
-
-  public DexProgramClass getDefinition() {
-    return clazz;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticReference.java b/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticReference.java
deleted file mode 100644
index 715e790..0000000
--- a/src/main/java/com/android/tools/r8/synthesis/LegacySyntheticReference.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.synthesis;
-
-import com.android.tools.r8.FeatureSplit;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
-import java.util.Set;
-
-public class LegacySyntheticReference implements Rewritable<LegacySyntheticReference> {
-  private final DexType type;
-  private final Set<DexType> contexts;
-  private final FeatureSplit featureSplit;
-
-  public LegacySyntheticReference(DexType type, Set<DexType> contexts, FeatureSplit featureSplit) {
-    this.type = type;
-    this.contexts = contexts;
-    this.featureSplit = featureSplit;
-  }
-
-  @Override
-  public DexType getHolder() {
-    return type;
-  }
-
-  public Set<DexType> getContexts() {
-    return contexts;
-  }
-
-  public FeatureSplit getFeatureSplit() {
-    return featureSplit;
-  }
-
-  @Override
-  public LegacySyntheticReference rewrite(NonIdentityGraphLens lens) {
-    DexType rewrittenType = lens.lookupType(type);
-    Set<DexType> rewrittenContexts = lens.rewriteTypes(getContexts());
-    if (type == rewrittenType && contexts.equals(rewrittenContexts)) {
-      return this;
-    }
-    return new LegacySyntheticReference(rewrittenType, rewrittenContexts, featureSplit);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
index 3bf8883..9fe1fb9 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
@@ -155,7 +155,7 @@
   public String toString() {
     return "SynthesizingContext{"
         + getSynthesizingContextType()
-        + (featureSplit != FeatureSplit.BASE ? ", feature:" + featureSplit : "")
+        + (!featureSplit.isBase() ? ", feature:" + featureSplit : "")
         + "}";
   }
 
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 0ce464d..9548f78 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -269,8 +269,7 @@
         new CommittedItems(
             SyntheticItems.INVALID_ID_AFTER_SYNTHETIC_FINALIZATION,
             application,
-            new CommittedSyntheticsCollection(
-                committed.getLegacyTypes(), finalMethods, finalClasses, finalInputSynthetics),
+            new CommittedSyntheticsCollection(finalMethods, finalClasses, finalInputSynthetics),
             ImmutableList.of()),
         syntheticFinalizationGraphLens,
         PrunedItems.builder().setPrunedApp(application).addRemovedClasses(prunedSynthetics).build(),
@@ -330,13 +329,6 @@
   private boolean verifyOneSyntheticPerSyntheticClass() {
     Set<DexType> seen = Sets.newIdentityHashSet();
     committed
-        .getLegacyTypes()
-        .forEach(
-            (type, references) -> {
-              assert seen.add(type);
-              assert references.size() == 1;
-            });
-    committed
         .getNonLegacyClasses()
         .forEach(
             (type, references) -> {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index caf0f42..f8e241b 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -41,7 +41,6 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
@@ -59,23 +58,17 @@
 
   /** Collection of pending items. */
   private static class PendingSynthetics {
-    /**
-     * Thread safe collection of synthesized classes that are not yet committed to the application.
-     *
-     * <p>TODO(b/158159959): Remove legacy support.
-     */
-    private final Map<DexType, LegacySyntheticDefinition> legacyClasses = new ConcurrentHashMap<>();
 
     /** Thread safe collection of synthetic items not yet committed to the application. */
     private final ConcurrentHashMap<DexType, SyntheticDefinition<?, ?, ?>> nonLegacyDefinitions =
         new ConcurrentHashMap<>();
 
     boolean isEmpty() {
-      return legacyClasses.isEmpty() && nonLegacyDefinitions.isEmpty();
+      return nonLegacyDefinitions.isEmpty();
     }
 
     boolean containsType(DexType type) {
-      return legacyClasses.containsKey(type) || nonLegacyDefinitions.containsKey(type);
+      return nonLegacyDefinitions.containsKey(type);
     }
 
     boolean containsTypeOfKind(DexType type, SyntheticKind kind) {
@@ -84,22 +77,17 @@
     }
 
     boolean verifyNotRewritten(NonIdentityGraphLens lens) {
-      assert legacyClasses.keySet().equals(lens.rewriteTypes(legacyClasses.keySet()));
       assert nonLegacyDefinitions.keySet().equals(lens.rewriteTypes(nonLegacyDefinitions.keySet()));
       return true;
     }
 
     Collection<DexProgramClass> getAllProgramClasses() {
-      List<DexProgramClass> allPending =
-          new ArrayList<>(nonLegacyDefinitions.size() + legacyClasses.size());
+      List<DexProgramClass> allPending = new ArrayList<>(nonLegacyDefinitions.size());
       for (SyntheticDefinition<?, ?, ?> item : nonLegacyDefinitions.values()) {
         if (item.isProgramDefinition()) {
           allPending.add(item.asProgramDefinition().getHolder());
         }
       }
-      for (LegacySyntheticDefinition legacy : legacyClasses.values()) {
-        allPending.add(legacy.getDefinition());
-      }
       return Collections.unmodifiableList(allPending);
     }
   }
@@ -189,22 +177,16 @@
   public DexClass definitionFor(DexType type, Function<DexType, DexClass> baseDefinitionFor) {
     DexClass clazz = null;
     SyntheticKind kind = null;
-    LegacySyntheticDefinition legacyItem = pending.legacyClasses.get(type);
-    if (legacyItem != null) {
-      clazz = legacyItem.getDefinition();
-    } else {
-      SyntheticDefinition<?, ?, ?> item = pending.nonLegacyDefinitions.get(type);
-      if (item != null) {
-        clazz = item.getHolder();
-        kind = item.getKind();
-        assert clazz.isProgramClass() == item.isProgramDefinition();
-        assert clazz.isClasspathClass() == item.isClasspathDefinition();
-      }
+    SyntheticDefinition<?, ?, ?> item = pending.nonLegacyDefinitions.get(type);
+    if (item != null) {
+      clazz = item.getHolder();
+      kind = item.getKind();
+      assert clazz.isProgramClass() == item.isProgramDefinition();
+      assert clazz.isClasspathClass() == item.isClasspathDefinition();
     }
     if (clazz != null) {
-      assert legacyItem != null || kind != null;
-      assert baseDefinitionFor.apply(type) == null
-              || (kind != null && kind.mayOverridesNonProgramType)
+      assert kind != null;
+      assert baseDefinitionFor.apply(type) == null || kind.mayOverridesNonProgramType
           : "Pending synthetic definition also present in the active program: " + type;
       return clazz;
     }
@@ -227,10 +209,6 @@
     return committed.containsType(type);
   }
 
-  private boolean isLegacyCommittedSynthetic(DexType type) {
-    return committed.containsLegacyType(type);
-  }
-
   private boolean isNonLegacyCommittedSynthetic(DexType type) {
     return committed.containsNonLegacyType(type);
   }
@@ -239,22 +217,10 @@
     return pending.containsType(type);
   }
 
-  private boolean isLegacyPendingSynthetic(DexType type) {
-    return pending.legacyClasses.containsKey(type);
-  }
-
   private boolean isNonLegacyPendingSynthetic(DexType type) {
     return pending.nonLegacyDefinitions.containsKey(type);
   }
 
-  public boolean isLegacySyntheticClass(DexType type) {
-    return isLegacyCommittedSynthetic(type) || isLegacyPendingSynthetic(type);
-  }
-
-  public boolean isLegacySyntheticClass(DexProgramClass clazz) {
-    return isLegacySyntheticClass(clazz.getType());
-  }
-
   public boolean isNonLegacySynthetic(DexProgramClass clazz) {
     return isNonLegacySynthetic(clazz.type);
   }
@@ -291,7 +257,7 @@
   }
 
   public boolean isSyntheticClass(DexType type) {
-    return isLegacySyntheticClass(type) || isNonLegacySynthetic(type);
+    return isNonLegacySynthetic(type);
   }
 
   public boolean isSyntheticClass(DexProgramClass clazz) {
@@ -306,21 +272,14 @@
     return committed.containsSyntheticInput(clazz.getType());
   }
 
-  public FeatureSplit getContextualFeatureSplit(DexType type) {
-    if (pending.legacyClasses.containsKey(type)) {
-      LegacySyntheticDefinition definition = pending.legacyClasses.get(type);
-      return definition.getFeatureSplit();
-    }
-    if (committed.containsLegacyType(type)) {
-      List<LegacySyntheticReference> types = committed.getLegacyTypes(type);
-      if (types.isEmpty()) {
-        return null;
-      }
-      assert verifyAllHaveSameFeature(types, LegacySyntheticReference::getFeatureSplit);
-      return types.get(0).getFeatureSplit();
-    }
+  public FeatureSplit getContextualFeatureSplit(
+      DexType type, ClassToFeatureSplitMap classToFeatureSplitMap) {
     if (isSyntheticOfKind(type, SyntheticKind.ENUM_UNBOXING_SHARED_UTILITY_CLASS)) {
-      return FeatureSplit.BASE;
+      // Use the startup base if there is one, such that we don't merge non-startup classes with the
+      // shared utility class in case it is used during startup. The use of base startup allows for
+      // merging startup classes with the shared utility class, however, which could be bad for
+      // startup if the shared utility class is not used during startup.
+      return classToFeatureSplitMap.getBaseStartup();
     }
     List<SynthesizingContext> contexts = getSynthesizingContexts(type);
     if (contexts.isEmpty()) {
@@ -358,13 +317,6 @@
     ImmutableList.Builder<DexType> builder = ImmutableList.builder();
     forEachSynthesizingContext(
         type, synthesizingContext -> builder.add(synthesizingContext.getSynthesizingContextType()));
-    for (LegacySyntheticReference legacyReference : committed.getLegacyTypes(type)) {
-      builder.addAll(legacyReference.getContexts());
-    }
-    LegacySyntheticDefinition legacyDefinition = pending.legacyClasses.get(type);
-    if (legacyDefinition != null) {
-      builder.addAll(legacyDefinition.getContexts());
-    }
     return builder.build();
   }
 
@@ -416,10 +368,6 @@
     return true;
   }
 
-  public Collection<DexProgramClass> getLegacyPendingClasses() {
-    return ListUtils.map(pending.legacyClasses.values(), LegacySyntheticDefinition::getDefinition);
-  }
-
   private SynthesizingContext getSynthesizingContext(
       ProgramDefinition context, AppView<?> appView) {
     return getSynthesizingContext(
@@ -451,29 +399,6 @@
 
   // Addition and creation of synthetic items.
 
-  public void addLegacySyntheticClassForLibraryDesugaring(DexProgramClass clazz) {
-    internalAddLegacySyntheticClass(clazz);
-    // No context information is added for library context.
-    // This is intended only to support desugared-library compilation.
-  }
-
-  // TODO(b/158159959): Remove the usage of this direct class addition.
-  public void addLegacySyntheticClass(
-      DexProgramClass clazz, ProgramDefinition context, FeatureSplit featureSplit) {
-    LegacySyntheticDefinition legacyItem = internalAddLegacySyntheticClass(clazz);
-    legacyItem.addContext(context, featureSplit);
-  }
-
-  private LegacySyntheticDefinition internalAddLegacySyntheticClass(DexProgramClass clazz) {
-    assert !isCommittedSynthetic(clazz.type);
-    assert !pending.nonLegacyDefinitions.containsKey(clazz.type);
-    LegacySyntheticDefinition legacyItem =
-        pending.legacyClasses.computeIfAbsent(
-            clazz.getType(), type -> new LegacySyntheticDefinition(clazz));
-    assert legacyItem.getDefinition() == clazz;
-    return legacyItem;
-  }
-
   private DexProgramClass internalEnsureDexProgramClass(
       SyntheticKind kind,
       Consumer<SyntheticProgramClassBuilder> classConsumer,
@@ -824,9 +749,6 @@
     DexApplication application = prunedItems.getPrunedApp();
     Set<DexType> removedClasses = prunedItems.getNoLongerSyntheticItems();
     CommittedSyntheticsCollection.Builder builder = committed.builder();
-    // Legacy synthetics must already have been committed to the app.
-    assert verifyClassesAreInApp(application, pending.legacyClasses.values());
-    builder.addLegacyClasses(pending.legacyClasses);
     // Compute the synthetic additions and add them to the application.
     ImmutableList<DexType> committedProgramTypes;
     DexApplication amendedApplication;
@@ -863,15 +785,6 @@
         committedProgramTypes);
   }
 
-  private static boolean verifyClassesAreInApp(
-      DexApplication app, Collection<LegacySyntheticDefinition> classes) {
-    for (LegacySyntheticDefinition item : classes) {
-      DexProgramClass clazz = item.getDefinition();
-      assert app.programDefinitionFor(clazz.type) != null : "Missing synthetic: " + clazz.type;
-    }
-    return true;
-  }
-
   public void writeAttributeIfIntermediateSyntheticClass(
       ClassWriter writer, DexProgramClass clazz, AppView<?> appView) {
     if (!appView.options().intermediate || !appView.options().isGeneratingClassFiles()) {
diff --git a/src/main/java/com/android/tools/r8/utils/AccessUtils.java b/src/main/java/com/android/tools/r8/utils/AccessUtils.java
new file mode 100644
index 0000000..0498949
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/AccessUtils.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexDefinitionSupplier;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+
+public class AccessUtils {
+
+  public static boolean isAccessibleInSameContextsAs(
+      DexType newType, DexType oldType, DexDefinitionSupplier definitions) {
+    DexItemFactory dexItemFactory = definitions.dexItemFactory();
+    DexType newBaseType = newType.toBaseType(dexItemFactory);
+    if (!newBaseType.isClassType()) {
+      return true;
+    }
+    DexClass newBaseClass = definitions.definitionFor(newBaseType);
+    if (newBaseClass == null) {
+      return false;
+    }
+    if (newBaseClass.isPublic()) {
+      return true;
+    }
+    DexType oldBaseType = oldType.toBaseType(dexItemFactory);
+    assert oldBaseType.isClassType();
+    DexClass oldBaseClass = definitions.definitionFor(oldBaseType);
+    if (oldBaseClass == null || oldBaseClass.isPublic()) {
+      return false;
+    }
+    return newBaseType.isSamePackage(oldBaseType);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index b1a2757..959238e 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.experimental.startup.StartupConfiguration;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
 import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -528,6 +529,7 @@
           dumpProgramResources(
               dumpProgramFileName,
               options.getFeatureSplitConfiguration(),
+              options.getStartupConfiguration(),
               nextDexIndex,
               out,
               reporter,
@@ -581,6 +583,7 @@
   private int dumpProgramResources(
       String archiveName,
       FeatureSplitConfiguration featureSplitConfiguration,
+      StartupConfiguration startupConfiguration,
       int nextDexIndex,
       ZipOutputStream out,
       Reporter reporter,
@@ -595,7 +598,7 @@
     try {
       ClassToFeatureSplitMap classToFeatureSplitMap =
           ClassToFeatureSplitMap.createInitialClassToFeatureSplitMap(
-              dexItemFactory, featureSplitConfiguration, reporter);
+              dexItemFactory, featureSplitConfiguration, startupConfiguration, reporter);
       if (featureSplitConfiguration != null) {
         for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
           ByteArrayOutputStream archiveByteStream = new ByteArrayOutputStream();
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 de70549..fadaaa0 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.errors.MissingNestHostNestDesugarDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
+import com.android.tools.r8.experimental.startup.StartupConfiguration;
 import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
@@ -159,6 +160,7 @@
 
   public DataResourceConsumer dataResourceConsumer;
   public FeatureSplitConfiguration featureSplitConfiguration;
+  public StartupConfiguration startupConfiguration;
 
   public List<Consumer<InspectorImpl>> outputInspections = Collections.emptyList();
 
@@ -1637,7 +1639,6 @@
     public boolean allowInvokeErrors = false;
     public boolean allowUnnecessaryDontWarnWildcards = true;
     public boolean allowUnusedDontWarnRules = true;
-    public boolean disableL8AnnotationRemoval = false;
     public boolean reportUnusedProguardConfigurationRules = false;
     public boolean alwaysUseExistingAccessInfoCollectionsInMemberRebinding = true;
     public boolean alwaysUsePessimisticRegisterAllocation = false;
diff --git a/src/main/java/com/android/tools/r8/utils/Timing.java b/src/main/java/com/android/tools/r8/utils/Timing.java
index c1046e9..5367c26 100644
--- a/src/main/java/com/android/tools/r8/utils/Timing.java
+++ b/src/main/java/com/android/tools/r8/utils/Timing.java
@@ -371,6 +371,15 @@
     stack.push(child);
   }
 
+  public <E extends Exception> void time(String title, ThrowingAction<E> action) throws E {
+    begin(title);
+    try {
+      action.execute();
+    } finally {
+      end();
+    }
+  }
+
   public <T> T time(String title, Supplier<T> supplier) {
     begin(title);
     try {
diff --git a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
index 3cd4941..9b81396 100644
--- a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
+++ b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
@@ -6,6 +6,7 @@
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.MAX_SUPPORTED_VERSION;
 import static com.android.tools.r8.ToolHelper.isWindows;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -303,9 +304,17 @@
 
   /** Compile asserting success and return the output path. */
   public Path compile() throws IOException {
+    return compile(false);
+  }
+
+  public Path compile(boolean expectingFailure) throws IOException {
     Path output = getOrCreateOutputPath();
     ProcessResult result = compileInternal(output);
-    assertEquals(result.toString(), result.exitCode, 0);
+    if (expectingFailure) {
+      assertNotEquals(result.toString(), result.exitCode, 0);
+    } else {
+      assertEquals(result.toString(), result.exitCode, 0);
+    }
     return output;
   }
 
diff --git a/src/test/java/com/android/tools/r8/KotlinTestParameters.java b/src/test/java/com/android/tools/r8/KotlinTestParameters.java
index 2ac3f87..0ddd80e 100644
--- a/src/test/java/com/android/tools/r8/KotlinTestParameters.java
+++ b/src/test/java/com/android/tools/r8/KotlinTestParameters.java
@@ -52,6 +52,10 @@
     return !isNewerThanOrEqualTo(otherVersion);
   }
 
+  public boolean isOlderThanMinSupported() {
+    return isOlderThan(KotlinCompilerVersion.MIN_SUPPORTED_VERSION);
+  }
+
   public boolean isFirst() {
     return index == 0;
   }
@@ -68,6 +72,7 @@
   public static class Builder {
 
     private Predicate<KotlinCompilerVersion> compilerFilter = c -> false;
+    private Predicate<KotlinCompilerVersion> oldCompilerFilter = c -> true;
     private Predicate<KotlinTargetVersion> targetVersionFilter = t -> false;
     private boolean withDevCompiler =
         System.getProperty("com.android.tools.r8.kotlincompilerdev") != null;
@@ -105,11 +110,21 @@
       return this;
     }
 
+    public Builder withOldCompilers() {
+      this.withOldCompilers = true;
+      return this;
+    }
+
     public Builder withOldCompilersIfSet() {
       assumeTrue(withOldCompilers);
       return this;
     }
 
+    public Builder withOldCompilersStartingFrom(KotlinCompilerVersion minOldVersion) {
+      oldCompilerFilter = oldCompilerFilter.and(v -> v.isGreaterThanOrEqualTo(minOldVersion));
+      return this;
+    }
+
     public Builder withAllTargetVersions() {
       withTargetVersionFilter(t -> t != KotlinTargetVersion.NONE);
       return this;
@@ -139,8 +154,8 @@
         compilerVersions =
             Arrays.stream(KotlinCompilerVersion.values())
                 .filter(c -> c.isLessThan(KotlinCompilerVersion.MIN_SUPPORTED_VERSION))
+                .filter(c -> oldCompilerFilter.test(c))
                 .collect(Collectors.toList());
-
       } else {
         compilerVersions =
             KotlinCompilerVersion.getSupported().stream()
@@ -162,7 +177,7 @@
           }
         }
       }
-      assert !testParameters.isEmpty();
+      assert !testParameters.isEmpty() || withOldCompilers;
       return new KotlinTestParametersCollection(testParameters);
     }
   }
diff --git a/src/test/java/com/android/tools/r8/L8TestBuilder.java b/src/test/java/com/android/tools/r8/L8TestBuilder.java
index 072da5a..e672f87 100644
--- a/src/test/java/com/android/tools/r8/L8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/L8TestBuilder.java
@@ -139,11 +139,6 @@
     return this;
   }
 
-  public L8TestBuilder setDisableL8AnnotationRemoval(boolean disableL8AnnotationRemoval) {
-    return addOptionsModifier(
-        options -> options.testing.disableL8AnnotationRemoval = disableL8AnnotationRemoval);
-  }
-
   public L8TestCompileResult compile()
       throws IOException, CompilationFailedException, ExecutionException {
     // We wrap exceptions in a RuntimeException to call this from a lambda.
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index f0c648b..ccdd0ca 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -2237,7 +2237,7 @@
       builder.appendArtOption("-Xnoimage-dex2oat");
     }
     for (String s : ToolHelper.getBootLibs(dexVm)) {
-      builder.appendBootClassPath(new File(s).getCanonicalPath());
+      builder.appendBootClasspath(new File(s).getCanonicalPath());
     }
     builder.setMainClass(JUNIT_TEST_RUNNER);
     builder.appendProgramArgument(fullClassName);
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 0480694..b96cef4 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -179,7 +179,7 @@
     for (int i = 2; i < args.length; i++) {
       args[i] = featureDependencies[i - 2].toString();
     }
-    return runArt(runtime, additionalRunClassPath, mainClassSubject.getFinalName(), args);
+    return runArt(runtime, mainClassSubject.getFinalName(), args);
   }
 
   public String getProguardMap() {
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index 9ab97e7..af079d9 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ObjectArrays;
+import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
 import java.nio.file.Path;
@@ -56,6 +57,7 @@
   public final int minApiLevel;
   private final OutputMode outputMode;
   final List<Path> additionalRunClassPath = new ArrayList<>();
+  final List<Path> additionalBootClasspath = new ArrayList<>();
   final List<String> vmArguments = new ArrayList<>();
   private boolean withArt6Plus64BitsLib = false;
   private boolean withArtFrameworks = true;
@@ -164,12 +166,10 @@
       case DEX:
         return runArt(
             new DexRuntime(ToolHelper.getDexVm()),
-            additionalRunClassPath,
             mainClassSubject.getFinalName());
       case CF:
         return runJava(
             TestRuntime.getDefaultJavaRuntime(),
-            additionalRunClassPath,
             mainClassSubject.getFinalName());
       default:
         throw new Unreachable();
@@ -207,12 +207,11 @@
     }
     assertThat("Did you forget a keep rule for the main method?", mainClassSubject, isPresent());
     if (runtime.isDex()) {
-      return runArt(runtime, additionalRunClassPath, mainClassSubject.getFinalName(), args);
+      return runArt(runtime, mainClassSubject.getFinalName(), args);
     }
     assert runtime.isCf();
     return runJava(
         runtime,
-        additionalRunClassPath,
         ObjectArrays.concat(mainClassSubject.getFinalName(), args));
   }
 
@@ -256,6 +255,38 @@
     }
   }
 
+  public CR addBootClasspathClasses(Class<?>... classes) throws Exception {
+    return addBootClasspathClasses(Arrays.asList(classes));
+  }
+
+  public CR addBootClasspathClasses(List<Class<?>> classes) throws Exception {
+    if (getBackend() == Backend.DEX) {
+      additionalBootClasspath.add(
+          testForD8(state.getTempFolder())
+              .addProgramClasses(classes)
+              .setMinApi(minApiLevel)
+              .compile()
+              .writeToZip());
+      return self();
+    }
+    assert getBackend() == Backend.CF;
+    try {
+      Path path = state.getNewTempFolder().resolve("runtime-classes.jar");
+      ArchiveConsumer consumer = new ArchiveConsumer(path);
+      for (Class<?> clazz : classes) {
+        consumer.accept(
+            ByteDataView.of(ToolHelper.getClassAsBytes(clazz)),
+            DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()),
+            null);
+      }
+      consumer.finished(null);
+      additionalBootClasspath.add(path);
+      return self();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   public CR addDesugaredCoreLibraryRunClassPath(
       Function<AndroidApiLevel, Path> classPathSupplier, AndroidApiLevel minAPILevel) {
     addRunClasspathFiles(classPathSupplier.apply(minAPILevel));
@@ -523,30 +554,29 @@
     }
   }
 
-  private RR runJava(TestRuntime runtime, List<Path> additionalClassPath, String... arguments)
-      throws IOException {
+  private RR runJava(TestRuntime runtime, String... arguments) throws IOException {
     assert runtime != null;
     Path out = state.getNewTempFolder().resolve("out.zip");
     app.writeToZip(out, OutputMode.ClassFile);
-    List<Path> classPath = ImmutableList.<Path>builder()
-        .addAll(additionalClassPath)
-        .add(out)
-        .build();
-    ProcessResult result = ToolHelper.runJava(runtime.asCf(), vmArguments, classPath, arguments);
+    List<Path> classPath =
+        ImmutableList.<Path>builder().addAll(additionalRunClassPath).add(out).build();
+    ProcessResult result =
+        ToolHelper.runJava(
+            runtime.asCf(), vmArguments, additionalBootClasspath, classPath, arguments);
     return createRunResult(runtime, result);
   }
 
-  RR runArt(
-      TestRuntime runtime, List<Path> additionalClassPath, String mainClass, String... arguments)
-      throws IOException {
+  RR runArt(TestRuntime runtime, String mainClass, String... arguments) throws IOException {
     DexVm vm = runtime.asDex().getVm();
     // TODO(b/127785410): Always assume a non-null runtime.
     Path out = state.getNewTempFolder().resolve("out.zip");
     app.writeToZip(out, OutputMode.DexIndexed);
-    List<String> classPath = ImmutableList.<String>builder()
-        .addAll(additionalClassPath.stream().map(Path::toString).collect(Collectors.toList()))
-        .add(out.toString())
-        .build();
+    List<String> classPath =
+        ImmutableList.<String>builder()
+            .addAll(
+                additionalRunClassPath.stream().map(Path::toString).collect(Collectors.toList()))
+            .add(out.toString())
+            .build();
     Consumer<ArtCommandBuilder> commandConsumer =
         withArt6Plus64BitsLib && vm.getVersion().isNewerThanOrEqual(DexVm.Version.V6_0_1)
             ? builder -> builder.appendArtOption("--64")
@@ -554,6 +584,22 @@
     commandConsumer =
         commandConsumer.andThen(
             builder -> {
+              if (!additionalBootClasspath.isEmpty()) {
+                DexVm dexVm = runtime.asDex().getVm();
+                if (dexVm.isNewerThan(DexVm.ART_4_4_4_HOST)) {
+                  builder.appendArtOption("-Ximage:/system/non/existent/image.art");
+                  builder.appendArtOption("-Xnoimage-dex2oat");
+                }
+                try {
+                  for (String s : ToolHelper.getBootLibs(dexVm)) {
+                    builder.appendBootClasspath(new File(s).getCanonicalPath());
+                  }
+                } catch (Exception e) {
+                  throw new RuntimeException();
+                }
+                additionalBootClasspath.forEach(
+                    path -> builder.appendBootClasspath(path.toString()));
+              }
               for (String vmArgument : vmArguments) {
                 builder.appendArtOption(vmArgument);
               }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 2d0cc12..57fabd1 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -422,7 +422,7 @@
     protected List<String> classpaths = new ArrayList<>();
     protected String mainClass;
     protected List<String> programArguments = new ArrayList<>();
-    protected List<String> bootClassPaths = new ArrayList<>();
+    protected List<String> bootClasspaths = new ArrayList<>();
     protected String executionDirectory;
 
     public CommandBuilder appendArtOption(String option) {
@@ -450,8 +450,8 @@
       return this;
     }
 
-    public CommandBuilder appendBootClassPath(String lib) {
-      bootClassPaths.add(lib);
+    public CommandBuilder appendBootClasspath(String lib) {
+      bootClasspaths.add(lib);
       return this;
     }
 
@@ -475,13 +475,13 @@
         builder.append(entry.getValue());
         result.add(builder.toString());
       }
+      if (!bootClasspaths.isEmpty()) {
+        result.add("-Xbootclasspath:" + String.join(":", bootClasspaths));
+      }
       if (!classpaths.isEmpty()) {
         result.add("-cp");
         result.add(String.join(":", classpaths));
       }
-      if (!bootClassPaths.isEmpty()) {
-        result.add("-Xbootclasspath:" + String.join(":", bootClassPaths));
-      }
       if (mainClass != null) {
         result.add(mainClass);
       }
@@ -555,7 +555,7 @@
           .setVmOptions(options)
           .setSystemProperties(systemProperties)
           .setClasspath(toFileList(classpaths))
-          .setBootClasspath(toFileList(bootClassPaths))
+          .setBootClasspath(toFileList(bootClasspaths))
           .setMainClass(mainClass)
           .setProgramArguments(programArguments);
     }
@@ -1416,12 +1416,28 @@
   public static ProcessResult runJava(
       CfRuntime runtime, List<String> vmArgs, List<Path> classpath, String... args)
       throws IOException {
-    String cp =
-        classpath.stream().map(Path::toString).collect(Collectors.joining(CLASSPATH_SEPARATOR));
+    return runJava(runtime, vmArgs, ImmutableList.of(), classpath, args);
+  }
+
+  public static ProcessResult runJava(
+      CfRuntime runtime,
+      List<String> vmArgs,
+      List<Path> bootClasspaths,
+      List<Path> classpath,
+      String... args)
+      throws IOException {
     List<String> cmdline = new ArrayList<>(Arrays.asList(runtime.getJavaExecutable().toString()));
     cmdline.addAll(vmArgs);
+    if (!bootClasspaths.isEmpty()) {
+      cmdline.add(
+          "-Xbootclasspath/a:"
+              + bootClasspaths.stream()
+                  .map(Path::toString)
+                  .collect(Collectors.joining(CLASSPATH_SEPARATOR)));
+    }
     cmdline.add("-cp");
-    cmdline.add(cp);
+    cmdline.add(
+        classpath.stream().map(Path::toString).collect(Collectors.joining(CLASSPATH_SEPARATOR)));
     cmdline.addAll(Arrays.asList(args));
     ProcessBuilder builder = new ProcessBuilder(cmdline);
     return runProcess(builder);
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMissingInterfaceTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMissingInterfaceTest.java
new file mode 100644
index 0000000..4f89220
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMissingInterfaceTest.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.Matchers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelInlineMissingInterfaceTest extends TestBase {
+
+  private final AndroidApiLevel libraryAdditionApiLevel = AndroidApiLevel.M;
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    boolean isApiLevel =
+        parameters.isDexRuntime()
+            && parameters.getApiLevel().isGreaterThanOrEqualTo(libraryAdditionApiLevel);
+    testForR8(parameters.getBackend())
+        .addProgramClasses(ProgramClass.class, VerificationError.class, Main.class)
+        .addLibraryClasses(LibraryClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .addAndroidBuildVersion()
+        .apply(setMockApiLevelForClass(LibraryClass.class, libraryAdditionApiLevel))
+        .apply(
+            setMockApiLevelForDefaultInstanceInitializer(
+                LibraryClass.class, libraryAdditionApiLevel))
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .compile()
+        .inspect(
+            inspector -> {
+              // Dex2Oat do not complain about missing super interfaces since these are not verified
+              // the same way as super types. We therefore expect the class to be absent.
+              assertThat(inspector.clazz(VerificationError.class), Matchers.isAbsent());
+            })
+        .applyIf(isApiLevel, b -> b.addRunClasspathClasses(LibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLinesIf(isApiLevel, "Hello World")
+        .assertSuccessWithOutputLinesIf(!isApiLevel, "Lower Api Level");
+  }
+
+  public interface LibraryClass {}
+
+  public static class ProgramClass implements LibraryClass {
+
+    @NeverInline
+    public void foo() {
+      System.out.println("Hello World");
+    }
+  }
+
+  public static class VerificationError {
+
+    public static ProgramClass create() {
+      return new ProgramClass();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      if (AndroidBuildVersion.VERSION >= 23) {
+        VerificationError.create().foo();
+      } else {
+        System.out.println("Lower Api Level");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMissingSuperTypeTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMissingSuperTypeTest.java
new file mode 100644
index 0000000..4e05ffb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineMissingSuperTypeTest.java
@@ -0,0 +1,92 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.Matchers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelInlineMissingSuperTypeTest extends TestBase {
+
+  private final AndroidApiLevel libraryAdditionApiLevel = AndroidApiLevel.M;
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    boolean isMockedApiLevel =
+        parameters.isDexRuntime()
+            && parameters.getApiLevel().isGreaterThanOrEqualTo(libraryAdditionApiLevel);
+    testForR8(parameters.getBackend())
+        .addProgramClasses(ProgramClass.class, VerificationError.class, Main.class)
+        .addLibraryClasses(LibraryClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .addAndroidBuildVersion()
+        .apply(setMockApiLevelForClass(LibraryClass.class, libraryAdditionApiLevel))
+        .apply(
+            setMockApiLevelForDefaultInstanceInitializer(
+                LibraryClass.class, libraryAdditionApiLevel))
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .compile()
+        .inspect(
+            inspector -> {
+              // TODO(b/207832084): Should be present and only inlined from M.
+              assertThat(inspector.clazz(VerificationError.class), Matchers.isAbsent());
+            })
+        .applyIf(isMockedApiLevel, b -> b.addRunClasspathClasses(LibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLinesIf(isMockedApiLevel, "Hello World")
+        .assertSuccessWithOutputLinesIf(!isMockedApiLevel, "Lower Api Level");
+  }
+
+  public static class LibraryClass {}
+
+  public static class ProgramClass extends LibraryClass {
+
+    @NeverInline
+    public void foo() {
+      System.out.println("Hello World");
+    }
+  }
+
+  public static class VerificationError {
+
+    public static ProgramClass create() {
+      return new ProgramClass();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      if (AndroidBuildVersion.VERSION >= 23) {
+        VerificationError.create().foo();
+      } else {
+        System.out.println("Lower Api Level");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassTest.java
new file mode 100644
index 0000000..4125cc2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockClassTest.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.Matchers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelMockClassTest extends TestBase {
+
+  private final AndroidApiLevel mockLevel = AndroidApiLevel.M;
+
+  @Parameter public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // TODO(b/197078995): Make this work on 12.
+    assumeFalse(
+        parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isEqualTo(Version.V12_0_0));
+    boolean isMockApiLevel =
+        parameters.isDexRuntime() && parameters.getApiLevel().isGreaterThanOrEqualTo(mockLevel);
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addLibraryClasses(LibraryClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addAndroidBuildVersion()
+        .apply(setMockApiLevelForClass(LibraryClass.class, mockLevel))
+        .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, mockLevel))
+        .compile()
+        .applyIf(
+            parameters.isDexRuntime()
+                && parameters.getRuntime().maxSupportedApiLevel().isGreaterThanOrEqualTo(mockLevel),
+            b -> b.addBootClasspathClasses(LibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLinesIf(isMockApiLevel, "LibraryClass::foo")
+        .assertSuccessWithOutputLinesIf(!isMockApiLevel, "Hello World")
+        .inspect(
+            inspector ->
+                // TODO(b/204982782): These should be stubbed for api-level 1-23.
+                assertThat(inspector.clazz(LibraryClass.class), Matchers.isAbsent()));
+  }
+
+  // Only present from api level 23.
+  public static class LibraryClass {
+
+    public void foo() {
+      System.out.println("LibraryClass::foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      if (AndroidBuildVersion.VERSION >= 23) {
+        new LibraryClass().foo();
+      } else {
+        System.out.println("Hello World");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockInheritedClassTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockInheritedClassTest.java
new file mode 100644
index 0000000..46a9fe8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockInheritedClassTest.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.Matchers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelMockInheritedClassTest extends TestBase {
+
+  private final AndroidApiLevel mockLevel = AndroidApiLevel.M;
+
+  @Parameter public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // TODO(b/197078995): Make this work on 12.
+    assumeFalse(
+        parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isEqualTo(Version.V12_0_0));
+    boolean isMockApiLevel =
+        parameters.isDexRuntime() && parameters.getApiLevel().isGreaterThanOrEqualTo(mockLevel);
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, ProgramClass.class)
+        .addLibraryClasses(LibraryClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addKeepClassRules(ProgramClass.class)
+        .addAndroidBuildVersion()
+        .apply(setMockApiLevelForClass(LibraryClass.class, mockLevel))
+        .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, mockLevel))
+        .compile()
+        .applyIf(
+            parameters.isDexRuntime()
+                && parameters.getRuntime().maxSupportedApiLevel().isGreaterThanOrEqualTo(mockLevel),
+            b -> b.addBootClasspathClasses(LibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLinesIf(isMockApiLevel, "ProgramClass::foo")
+        .assertSuccessWithOutputLinesIf(!isMockApiLevel, "Hello World")
+        .inspect(
+            inspector ->
+                // TODO(b/204982782): These should be stubbed for api-level 1-23.
+                assertThat(inspector.clazz(LibraryClass.class), Matchers.isAbsent()));
+  }
+
+  // Only present from api level 23.
+  public static class LibraryClass {}
+
+  public static class ProgramClass extends LibraryClass {
+
+    public void foo() {
+      System.out.println("ProgramClass::foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      if (AndroidBuildVersion.VERSION >= 23) {
+        new ProgramClass().foo();
+      } else {
+        System.out.println("Hello World");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockMethodMissingClassTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockMethodMissingClassTest.java
new file mode 100644
index 0000000..6b91898
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockMethodMissingClassTest.java
@@ -0,0 +1,128 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.Matchers;
+import java.lang.reflect.Method;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelMockMethodMissingClassTest extends TestBase {
+
+  private final AndroidApiLevel initialLibraryMockLevel = AndroidApiLevel.M;
+  private final AndroidApiLevel finalLibraryMethodLevel = AndroidApiLevel.O_MR1;
+
+  @Parameter public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // TODO(b/197078995): Make this work on 12.
+    assumeFalse(
+        parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isEqualTo(Version.V12_0_0));
+    boolean preMockApis =
+        parameters.isCfRuntime() || parameters.getApiLevel().isLessThan(initialLibraryMockLevel);
+    boolean postMockApis =
+        !preMockApis && parameters.getApiLevel().isGreaterThanOrEqualTo(finalLibraryMethodLevel);
+    boolean betweenMockApis = !preMockApis && !postMockApis;
+    Method addedOn23 = LibraryClass.class.getMethod("addedOn23");
+    Method adeddOn27 = LibraryClass.class.getMethod("addedOn27");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addLibraryClasses(LibraryClass.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addAndroidBuildVersion()
+        .apply(setMockApiLevelForClass(LibraryClass.class, initialLibraryMockLevel))
+        .apply(
+            setMockApiLevelForDefaultInstanceInitializer(
+                LibraryClass.class, initialLibraryMockLevel))
+        .apply(setMockApiLevelForMethod(addedOn23, initialLibraryMockLevel))
+        .apply(setMockApiLevelForMethod(adeddOn27, finalLibraryMethodLevel))
+        .compile()
+        .applyIf(
+            parameters.isDexRuntime()
+                && parameters
+                    .getRuntime()
+                    .maxSupportedApiLevel()
+                    .isGreaterThanOrEqualTo(initialLibraryMockLevel),
+            b -> b.addBootClasspathClasses(LibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLinesIf(preMockApis, "Hello World")
+        .assertSuccessWithOutputLinesIf(
+            betweenMockApis,
+            "LibraryClass::addedOn23",
+            "LibraryClass::missingAndReferenced",
+            "Hello World")
+        .assertSuccessWithOutputLinesIf(
+            postMockApis,
+            "LibraryClass::addedOn23",
+            "LibraryClass::missingAndReferenced",
+            "LibraryCLass::addedOn27",
+            "Hello World")
+        .inspect(
+            inspector -> {
+              // TODO(b/204982782): Should be stubbed for api-level 1-23 with methods.
+              assertThat(
+                  inspector.clazz(ApiModelMockClassTest.LibraryClass.class), Matchers.isAbsent());
+            });
+  }
+
+  // Only present from api level 23.
+  public static class LibraryClass {
+
+    public void addedOn23() {
+      System.out.println("LibraryClass::addedOn23");
+    }
+
+    public void addedOn27() {
+      System.out.println("LibraryCLass::addedOn27");
+    }
+
+    public void missingAndReferenced() {
+      System.out.println("LibraryClass::missingAndReferenced");
+    }
+
+    public void missingNotReferenced() {
+      System.out.println("LibraryClass::missingNotReferenced");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      if (AndroidBuildVersion.VERSION >= 23) {
+        LibraryClass libraryClass = new LibraryClass();
+        libraryClass.addedOn23();
+        libraryClass.missingAndReferenced();
+        if (AndroidBuildVersion.VERSION >= 27) {
+          libraryClass.addedOn27();
+        }
+      }
+      System.out.println("Hello World");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelMockSuperChainClassTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockSuperChainClassTest.java
new file mode 100644
index 0000000..285f3d0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelMockSuperChainClassTest.java
@@ -0,0 +1,113 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.Matchers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelMockSuperChainClassTest extends TestBase {
+
+  private final AndroidApiLevel mockApiLevel = AndroidApiLevel.N;
+  private final AndroidApiLevel lowerMockApiLevel = AndroidApiLevel.M;
+
+  @Parameter public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // TODO(b/197078995): Make this work on 12.
+    assumeFalse(
+        parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isEqualTo(Version.V12_0_0));
+    boolean isMockApiLevel =
+        parameters.isDexRuntime() && parameters.getApiLevel().isGreaterThanOrEqualTo(mockApiLevel);
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, ProgramClass.class)
+        .addLibraryClasses(LibraryClass.class, OtherLibraryClass.class, LibraryInterface.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addKeepClassRules(ProgramClass.class)
+        .addAndroidBuildVersion()
+        .apply(setMockApiLevelForClass(LibraryClass.class, lowerMockApiLevel))
+        .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, lowerMockApiLevel))
+        .apply(setMockApiLevelForClass(OtherLibraryClass.class, mockApiLevel))
+        .apply(setMockApiLevelForDefaultInstanceInitializer(OtherLibraryClass.class, mockApiLevel))
+        .apply(setMockApiLevelForClass(LibraryInterface.class, lowerMockApiLevel))
+        .compile()
+        .applyIf(
+            parameters.isDexRuntime()
+                && parameters
+                    .getRuntime()
+                    .maxSupportedApiLevel()
+                    .isGreaterThanOrEqualTo(lowerMockApiLevel),
+            b -> b.addBootClasspathClasses(LibraryClass.class, LibraryInterface.class))
+        .applyIf(
+            parameters.isDexRuntime()
+                && parameters
+                    .getRuntime()
+                    .maxSupportedApiLevel()
+                    .isGreaterThanOrEqualTo(mockApiLevel),
+            b -> b.addBootClasspathClasses(OtherLibraryClass.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLinesIf(isMockApiLevel, "ProgramClass::foo")
+        .assertSuccessWithOutputLinesIf(!isMockApiLevel, "Hello World")
+        .inspect(
+            inspector -> {
+              // TODO(b/204982782): These should be stubbed out for api-level 1-23.
+              assertThat(inspector.clazz(LibraryClass.class), not(Matchers.isPresent()));
+              assertThat(inspector.clazz(LibraryInterface.class), not(Matchers.isPresent()));
+              // TODO(b/204982782): This should be stubbed out for api-level 1-24.
+              assertThat(inspector.clazz(OtherLibraryClass.class), not(Matchers.isPresent()));
+            });
+  }
+
+  // Only present from api level 23.
+  public static class LibraryClass {}
+
+  // Only present from api level 23
+  public interface LibraryInterface {}
+
+  // Only present from api level 24
+  public static class OtherLibraryClass extends LibraryClass {}
+
+  public static class ProgramClass extends OtherLibraryClass implements LibraryInterface {
+
+    public void foo() {
+      System.out.println("ProgramClass::foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      if (AndroidBuildVersion.VERSION >= 24) {
+        new ProgramClass().foo();
+      } else {
+        System.out.println("Hello World");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfTryCatchReferenceTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfTryCatchReferenceTest.java
new file mode 100644
index 0000000..9565973
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelNoInliningOfTryCatchReferenceTest.java
@@ -0,0 +1,122 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.lang.reflect.Method;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelNoInliningOfTryCatchReferenceTest extends TestBase {
+
+  private final AndroidApiLevel exceptionApiLevel = AndroidApiLevel.L_MR1;
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Method tryCatch = TestClass.class.getDeclaredMethod("testTryCatch");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, TestClass.class, Caller.class, KeptClass.class)
+        .addLibraryClasses(ApiException.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMethodRules(
+            Reference.methodFromMethod(KeptClass.class.getDeclaredMethod("keptMethodThatMayThrow")))
+        .addKeepMainRule(Main.class)
+        .apply(setMockApiLevelForClass(ApiException.class, exceptionApiLevel))
+        .apply(setMockApiLevelForDefaultInstanceInitializer(ApiException.class, exceptionApiLevel))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .enableInliningAnnotations()
+        .addHorizontallyMergedClassesInspector(
+            horizontallyMergedClassesInspector -> {
+              if (parameters.isDexRuntime()
+                  && parameters.getApiLevel().isGreaterThanOrEqualTo(exceptionApiLevel)) {
+                horizontallyMergedClassesInspector.assertClassesMerged(
+                    TestClass.class, Caller.class);
+              } else {
+                horizontallyMergedClassesInspector.assertNoClassesMerged();
+              }
+            })
+        .apply(
+            ApiModelingTestHelper.addTracedApiReferenceLevelCallBack(
+                (reference, apiLevel) -> {
+                  if (reference.equals(Reference.methodFromMethod(tryCatch))) {
+                    assertEquals(
+                        exceptionApiLevel.max(
+                            parameters.isCfRuntime()
+                                ? AndroidApiLevel.B
+                                : parameters.getApiLevel()),
+                        apiLevel);
+                  }
+                }))
+        .compile()
+        .applyIf(
+            parameters.isCfRuntime()
+                || parameters
+                    .asDexRuntime()
+                    .getMinApiLevel()
+                    .isGreaterThanOrEqualTo(exceptionApiLevel),
+            b -> b.addRunClasspathClasses(ApiException.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello World");
+  }
+
+  public static class ApiException extends RuntimeException {}
+
+  public static class TestClass {
+
+    public static void testTryCatch() {
+      try {
+        KeptClass.keptMethodThatMayThrow();
+      } catch (ApiException e) {
+        System.out.println("Caught ApiException");
+      }
+    }
+  }
+
+  public static class KeptClass {
+
+    public static void keptMethodThatMayThrow() {
+      if (System.currentTimeMillis() == 0) {
+        throw new ApiException();
+      }
+      System.out.println("Hello World");
+    }
+  }
+
+  public static class Caller {
+
+    @NeverInline
+    public static void callTestClass() {
+      TestClass.testTryCatch();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Caller.callTestClass();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java
index 0e373e9..f1498b6 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/KeepNonVisibilityBridgeMethodsTest.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.AlwaysInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -51,6 +52,11 @@
             "  synthetic void registerObserver(...);",
             "}")
         .allowAccessModification()
+        .addAlwaysInliningAnnotations()
+        .addKeepRules(
+            "-alwaysinline class * { @"
+                + AlwaysInline.class.getTypeName()
+                + " !synthetic <methods>; }")
         .enableNeverClassInliningAnnotations()
         // TODO(b/120764902): MemberSubject.getOriginalName() is not working without the @NeverMerge
         //  annotation on DataAdapter.Observer.
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/SimpleObservableList.java b/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/SimpleObservableList.java
index e6eceb7..b99ee02 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/SimpleObservableList.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/bridgestokeep/SimpleObservableList.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.bridgeremoval.bridgestokeep;
 
+import com.android.tools.r8.AlwaysInline;
 import com.android.tools.r8.bridgeremoval.bridgestokeep.ObservableList.Observer;
 import java.util.ArrayList;
 import java.util.List;
@@ -13,6 +14,7 @@
 
   private List<O> observers = new ArrayList<>();
 
+  @AlwaysInline
   @Override
   public void registerObserver(O observer) {
     if (observer != null && observers != null && !observers.contains(observer)) {
diff --git a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
index cdecb76..12bbdf3 100644
--- a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
+++ b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
@@ -50,7 +50,10 @@
 
   private static final String HELLO_NAME = "hello.Hello";
   private static final String[] KEEP_HELLO = {
-    "-keep class " + HELLO_NAME + " {", "  public static void main(...);", "}",
+    "-keep class " + HELLO_NAME + " {",
+    "  public static void main(...);",
+    "}",
+    "-allowaccessmodification"
   };
 
   private static Pair<Path, Path> r8R8Debug;
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingWithStartupClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingWithStartupClassesTest.java
new file mode 100644
index 0000000..d594603
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingWithStartupClassesTest.java
@@ -0,0 +1,121 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.experimental.startup.StartupConfiguration;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.google.common.collect.ImmutableList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class HorizontalClassMergingWithStartupClassesTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public List<Class<?>> startupClasses;
+
+  @Parameters(name = "{0}, startup classes: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        ImmutableList.of(
+            Collections.emptyList(), ImmutableList.of(StartupA.class, StartupB.class)));
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassAndMembersRules(Main.class)
+        .addOptionsModification(
+            options -> {
+              DexItemFactory dexItemFactory = options.dexItemFactory();
+              options.startupConfiguration =
+                  new StartupConfiguration(
+                      startupClasses.stream()
+                          .map(clazz -> toDexType(clazz, dexItemFactory))
+                          .collect(Collectors.toList()));
+            })
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector
+                    .applyIf(
+                        startupClasses.isEmpty(),
+                        i ->
+                            i.assertIsCompleteMergeGroup(
+                                StartupA.class,
+                                StartupB.class,
+                                OnClickHandlerA.class,
+                                OnClickHandlerB.class),
+                        i ->
+                            i.assertIsCompleteMergeGroup(StartupA.class, StartupB.class)
+                                .assertIsCompleteMergeGroup(
+                                    OnClickHandlerA.class, OnClickHandlerB.class))
+                    .assertNoOtherClassesMerged())
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("StartupA", "StartupB");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      StartupA.foo();
+      StartupB.bar();
+    }
+
+    // @Keep
+    public void onClick() {
+      OnClickHandlerA.baz();
+      OnClickHandlerB.qux();
+    }
+  }
+
+  static class StartupA {
+
+    @NeverInline
+    static void foo() {
+      System.out.println("StartupA");
+    }
+  }
+
+  static class StartupB {
+
+    @NeverInline
+    static void bar() {
+      System.out.println("StartupB");
+    }
+  }
+
+  static class OnClickHandlerA {
+
+    @NeverInline
+    static void baz() {
+      System.out.println("IdleA");
+    }
+  }
+
+  static class OnClickHandlerB {
+
+    @NeverInline
+    static void qux() {
+      System.out.println("IdleB");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
index 645588c..e57aaa9 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
@@ -163,10 +163,6 @@
               L8TestBuilder::setDebug)
           .addOptionsModifier(optionsModifier)
           .setDesugarJDKLibsConfiguration(ToolHelper.DESUGAR_LIB_CONVERSIONS)
-          // If we compile extended library here, it means we use TestNG. TestNG requires
-          // annotations, hence we disable annotation removal. This implies that extra warnings are
-          // generated.
-          .setDisableL8AnnotationRemoval(!additionalProgramFiles.isEmpty())
           .compile()
           .applyIf(
               additionalProgramFiles.isEmpty(),
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeInterfaceWithRefinedReceiverTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeInterfaceWithRefinedReceiverTest.java
index d07e819..e3afc1c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeInterfaceWithRefinedReceiverTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeInterfaceWithRefinedReceiverTest.java
@@ -68,9 +68,9 @@
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
     if (method.getHolderType().toSourceString().endsWith("$C")) {
-      assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNotNull();
+      assert callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNotNull();
     } else {
-      assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNull();
+      assert callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNull();
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeVirtualWithRefinedReceiverTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeVirtualWithRefinedReceiverTest.java
index eb665f8..847bf55 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeVirtualWithRefinedReceiverTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeVirtualWithRefinedReceiverTest.java
@@ -63,9 +63,9 @@
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
     if (method.getHolderType().toSourceString().endsWith("$C")) {
-      assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNotNull();
+      assert callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNotNull();
     } else {
-      assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNull();
+      assert callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNull();
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeDirectNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeDirectNegativeTest.java
index ee612f8..f55c70b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeDirectNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeDirectNegativeTest.java
@@ -58,7 +58,7 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNotNull();
+    assert callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNotNull();
     assert callSiteOptimizationInfo.getAbstractArgumentValue(1).isUnknown();
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeInterfaceNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeInterfaceNegativeTest.java
index fed41d3..ca24f1f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeInterfaceNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeInterfaceNegativeTest.java
@@ -64,7 +64,7 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNotNull();
+    assert callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNotNull();
     assert callSiteOptimizationInfo.getAbstractArgumentValue(1).isUnknown();
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeInterfacePositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeInterfacePositiveTest.java
index 042ca95..39f33af 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeInterfacePositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeInterfacePositiveTest.java
@@ -64,7 +64,7 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNotNull();
+    assert callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNotNull();
     AbstractValue abstractValue = callSiteOptimizationInfo.getAbstractArgumentValue(1);
     assert abstractValue.isSingleStringValue()
         && abstractValue.asSingleStringValue().getDexString().toString().equals("nul");
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeStaticNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeStaticNegativeTest.java
index 4c1b255..5c80990 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeStaticNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeStaticNegativeTest.java
@@ -56,8 +56,8 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    assert callSiteOptimizationInfo.getDynamicUpperBoundType(0).isDefinitelyNotNull();
-    assert callSiteOptimizationInfo.getAbstractArgumentValue(0).isUnknown();
+    assertTrue(callSiteOptimizationInfo.getDynamicType(0).isNotNullType());
+    assertTrue(callSiteOptimizationInfo.getAbstractArgumentValue(0).isUnknown());
   }
 
   private void inspect(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeVirtualNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeVirtualNegativeTest.java
index ba515db5..51593ba 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeVirtualNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeVirtualNegativeTest.java
@@ -62,11 +62,15 @@
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
     if (methodName.equals("m")) {
-      assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNotNull();
+      assert callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNotNull();
       assert callSiteOptimizationInfo.getAbstractArgumentValue(1).isUnknown();
     } else {
       assert methodName.equals("test");
-      assert callSiteOptimizationInfo.getDynamicUpperBoundType(0).isDefinitelyNotNull();
+      assert callSiteOptimizationInfo
+          .getDynamicType(0)
+          .asDynamicTypeWithUpperBound()
+          .getDynamicUpperBoundType()
+          .isDefinitelyNotNull();
       assert callSiteOptimizationInfo.getAbstractArgumentValue(0).isUnknown();
     }
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeVirtualPositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeVirtualPositiveTest.java
index bbe45f0..1c5689d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeVirtualPositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/constants/InvokeVirtualPositiveTest.java
@@ -60,7 +60,7 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNotNull();
+    assert callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNotNull();
     AbstractValue abstractValue = callSiteOptimizationInfo.getAbstractArgumentValue(1);
     if (method.getHolderType().toSourceString().endsWith("$A")) {
       assert abstractValue.isSingleStringValue()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeDirectNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeDirectNegativeTest.java
index 86a89ed..76daffe 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeDirectNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeDirectNegativeTest.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -54,14 +53,12 @@
   }
 
   private void callSiteOptimizationInfoInspect(ProgramMethod method) {
-    assert method.getReference().name.toString().equals("test")
-        : "Unexpected revisit: " + method.toSourceString();
+    assertTrue(
+        "Unexpected revisit: " + method.toSourceString(),
+        method.getReference().name.toString().equals("test"));
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    TypeElement upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(1);
-    assert upperBoundType.isDefinitelyNotNull();
-    assert upperBoundType.isClassType()
-        && upperBoundType.asClassType().getClassType().toSourceString().endsWith("$Base");
+    assertTrue(callSiteOptimizationInfo.getDynamicType(1).isNotNullType());
   }
 
   private void inspect(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeDirectPositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeDirectPositiveTest.java
index 5b7fa84..5811471 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeDirectPositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeDirectPositiveTest.java
@@ -63,12 +63,20 @@
         method.getOptimizationInfo().getArgumentInfos();
     TypeElement upperBoundType;
     if (methodName.equals("test")) {
-      upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(1);
+      upperBoundType =
+          callSiteOptimizationInfo
+              .getDynamicType(1)
+              .asDynamicTypeWithUpperBound()
+              .getDynamicUpperBoundType();
     } else {
       // TODO(b/139246447): should avoid visiting <init>, which is trivial, default init!
       // For testing purpose, `Base` is not merged and kept. The system correctly caught that, when
       // the default initializer is invoked, the receiver had a refined type, `Sub1`.
-      upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(0);
+      upperBoundType =
+          callSiteOptimizationInfo
+              .getDynamicType(0)
+              .asDynamicTypeWithUpperBound()
+              .getDynamicUpperBoundType();
     }
     assert upperBoundType.isDefinitelyNotNull();
     assert upperBoundType.isClassType()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeInterfaceNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeInterfaceNegativeTest.java
index 4c72b52..9fe54b4 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeInterfaceNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeInterfaceNegativeTest.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -64,10 +63,7 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    TypeElement upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(1);
-    assert upperBoundType.isDefinitelyNotNull();
-    assert upperBoundType.isClassType()
-        && upperBoundType.asClassType().getClassType().toSourceString().endsWith("$Base");
+    assertTrue(callSiteOptimizationInfo.getDynamicType(1).isNotNullType());
   }
 
   private void inspect(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeInterfacePositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeInterfacePositiveTest.java
index 0c4b99a..fd6b29e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeInterfacePositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeInterfacePositiveTest.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -66,14 +67,18 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    TypeElement upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(1);
-    assert upperBoundType.isDefinitelyNotNull();
+    DynamicType dynamicType = callSiteOptimizationInfo.getDynamicType(1);
     if (method.getHolderType().toSourceString().endsWith("$A")) {
+      TypeElement upperBoundType =
+          callSiteOptimizationInfo
+              .getDynamicType(1)
+              .asDynamicTypeWithUpperBound()
+              .getDynamicUpperBoundType();
+      assert upperBoundType.isDefinitelyNotNull();
       assert upperBoundType.isClassType()
           && upperBoundType.asClassType().getClassType().toSourceString().endsWith("$Sub1");
     } else {
-      assert upperBoundType.isClassType()
-          && upperBoundType.asClassType().getClassType().toSourceString().endsWith("$Base");
+      assertTrue(dynamicType.isNotNullType());
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeStaticNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeStaticNegativeTest.java
index 0b46131..2055210 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeStaticNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeStaticNegativeTest.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -52,14 +51,12 @@
   }
 
   private void callSiteOptimizationInfoInspect(ProgramMethod method) {
-    assert method.getReference().name.toString().equals("test")
-        : "Unexpected revisit: " + method.toSourceString();
+    assertTrue(
+        "Unexpected revisit: " + method.toSourceString(),
+        method.getReference().name.toString().equals("test"));
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    TypeElement upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(0);
-    assert upperBoundType.isDefinitelyNotNull();
-    assert upperBoundType.isClassType()
-        && upperBoundType.asClassType().getClassType().toSourceString().endsWith("$Base");
+    assertTrue(callSiteOptimizationInfo.getDynamicType(0).isNotNullType());
   }
 
   private void inspect(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeStaticPositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeStaticPositiveTest.java
index dae8e30..a32daad 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeStaticPositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeStaticPositiveTest.java
@@ -63,7 +63,11 @@
     // TODO(b/139246447): should avoid visiting <init>, which is trivial, default init!
     // For testing purpose, `Base` is not merged and kept. The system correctly caught that, when
     // the default initializer is invoked, the receiver had a refined type, `Sub1`.
-    TypeElement upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(0);
+    TypeElement upperBoundType =
+        callSiteOptimizationInfo
+            .getDynamicType(0)
+            .asDynamicTypeWithUpperBound()
+            .getDynamicUpperBoundType();
     assert upperBoundType.isDefinitelyNotNull();
     assert upperBoundType.isClassType()
         && upperBoundType.asClassType().getClassType().toSourceString().endsWith("$Sub1");
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeVirtualNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeVirtualNegativeTest.java
index 1952e51..b419ad2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeVirtualNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeVirtualNegativeTest.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -62,13 +61,15 @@
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
     if (methodName.equals("m")) {
-      TypeElement upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(1);
-      assert upperBoundType.isDefinitelyNotNull();
-      assert upperBoundType.isClassType()
-          && upperBoundType.asClassType().getClassType().toSourceString().endsWith("$Base");
+      assertTrue(callSiteOptimizationInfo.getDynamicType(1).isNotNullType());
     } else {
-      assert methodName.equals("test");
-      assert callSiteOptimizationInfo.getDynamicUpperBoundType(0).isDefinitelyNotNull();
+      assertTrue(methodName.equals("test"));
+      assertTrue(
+          callSiteOptimizationInfo
+              .getDynamicType(0)
+              .asDynamicTypeWithUpperBound()
+              .getDynamicUpperBoundType()
+              .isDefinitelyNotNull());
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeVirtualPositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeVirtualPositiveTest.java
index e863304..99a3ddf 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeVirtualPositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/dynamicupperboundtype/InvokeVirtualPositiveTest.java
@@ -63,12 +63,20 @@
         method.getOptimizationInfo().getArgumentInfos();
     TypeElement upperBoundType;
     if (methodName.equals("m")) {
-      upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(1);
+      upperBoundType =
+          callSiteOptimizationInfo
+              .getDynamicType(1)
+              .asDynamicTypeWithUpperBound()
+              .getDynamicUpperBoundType();
     } else {
       // TODO(b/139246447): should avoid visiting <init>, which is trivial, default init!
       // For testing purpose, `Base` is not merged and kept. The system correctly caught that, when
       // the default initializer is invoked, the receiver had a refined type, `Sub1`.
-      upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(0);
+      upperBoundType =
+          callSiteOptimizationInfo
+              .getDynamicType(0)
+              .asDynamicTypeWithUpperBound()
+              .getDynamicUpperBoundType();
     }
     assert upperBoundType.isDefinitelyNotNull();
     assert upperBoundType.isClassType()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeDirectPositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeDirectPositiveTest.java
index dc8820b..fd605ac 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeDirectPositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeDirectPositiveTest.java
@@ -57,7 +57,7 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNotNull();
+    assert callSiteOptimizationInfo.getDynamicType(1).isNotNullType();
   }
 
   private void inspect(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeInterfaceNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeInterfaceNegativeTest.java
index f989c9f..a80798a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeInterfaceNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeInterfaceNegativeTest.java
@@ -64,7 +64,11 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    TypeElement upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(1);
+    TypeElement upperBoundType =
+        callSiteOptimizationInfo
+            .getDynamicType(1)
+            .asDynamicTypeWithUpperBound()
+            .getDynamicUpperBoundType();
     assert upperBoundType.isNullable();
     assert upperBoundType.isClassType()
         && upperBoundType.asClassType().getClassType().toSourceString().endsWith("$A");
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeInterfacePositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeInterfacePositiveTest.java
index 0c38b8f..b3988ee 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeInterfacePositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeInterfacePositiveTest.java
@@ -63,7 +63,7 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    assert callSiteOptimizationInfo.getDynamicUpperBoundType(1).isDefinitelyNotNull();
+    assertTrue(callSiteOptimizationInfo.getDynamicType(1).getNullability().isDefinitelyNotNull());
   }
 
   private void inspect(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeStaticPositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeStaticPositiveTest.java
index d386c2f..cf5cbe1 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeStaticPositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeStaticPositiveTest.java
@@ -55,7 +55,7 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    assert callSiteOptimizationInfo.getDynamicUpperBoundType(0).isDefinitelyNotNull();
+    assert callSiteOptimizationInfo.getDynamicType(0).isNotNullType();
   }
 
   private void inspect(CodeInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeVirtualNegativeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeVirtualNegativeTest.java
index e473e4a..08bd75c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeVirtualNegativeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeVirtualNegativeTest.java
@@ -62,13 +62,21 @@
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
     if (methodName.equals("m")) {
-      TypeElement upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(1);
+      TypeElement upperBoundType =
+          callSiteOptimizationInfo
+              .getDynamicType(1)
+              .asDynamicTypeWithUpperBound()
+              .getDynamicUpperBoundType();
       assert upperBoundType.isNullable();
       assert upperBoundType.isClassType()
           && upperBoundType.asClassType().getClassType().equals(method.getHolderType());
     } else {
       assert methodName.equals("test");
-      assert callSiteOptimizationInfo.getDynamicUpperBoundType(0).isDefinitelyNotNull();
+      assert callSiteOptimizationInfo
+          .getDynamicType(0)
+          .asDynamicTypeWithUpperBound()
+          .getDynamicUpperBoundType()
+          .isDefinitelyNotNull();
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeVirtualPositiveTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeVirtualPositiveTest.java
index 0ee8eca..c1c88a2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeVirtualPositiveTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/nullability/InvokeVirtualPositiveTest.java
@@ -60,7 +60,11 @@
         : "Unexpected revisit: " + method.toSourceString();
     CallSiteOptimizationInfo callSiteOptimizationInfo =
         method.getOptimizationInfo().getArgumentInfos();
-    TypeElement upperBoundType = callSiteOptimizationInfo.getDynamicUpperBoundType(1);
+    TypeElement upperBoundType =
+        callSiteOptimizationInfo
+            .getDynamicType(1)
+            .asDynamicTypeWithUpperBound()
+            .getDynamicUpperBoundType();
     assert upperBoundType.isClassType()
         && upperBoundType.asClassType().getClassType().toSourceString().endsWith("$A");
     if (method.getHolderType().toSourceString().endsWith("$A")) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineCatchHandlerWithLibraryTypeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineCatchHandlerWithLibraryTypeTest.java
index 5874af4..e5635d0 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineCatchHandlerWithLibraryTypeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineCatchHandlerWithLibraryTypeTest.java
@@ -93,11 +93,6 @@
         || parameters.getApiLevel().getLevel() < EXCEPTIONS.get(exception);
   }
 
-  private boolean compileTargetHasVerificationBug() {
-    // A CF target could target any API in the end.
-    return parameters.isCfRuntime() || parameters.getApiLevel().isLessThan(AndroidApiLevel.L);
-  }
-
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
@@ -131,7 +126,7 @@
     boolean mainHasInlinedCatchHandler =
         Streams.stream(classSubject.mainMethod().iterateTryCatches())
             .anyMatch(tryCatch -> tryCatch.isCatching(exception));
-    if (compileTargetHasVerificationBug() && compilationTargetIsMissingExceptionType()) {
+    if (compilationTargetIsMissingExceptionType()) {
       assertFalse(mainHasInlinedCatchHandler);
     } else {
       assertTrue(mainHasInlinedCatchHandler);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress131349148.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress131349148.java
index 68991bd..84b6917 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress131349148.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress131349148.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.inliner;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
@@ -72,9 +73,9 @@
         result.inspector().clazz(TestClassCallingMethodWithNonExisting.class);
     boolean hasCatchHandler =
         Streams.stream(classSubject.mainMethod().iterateTryCatches()).count() > 0;
-    int runtimeLevel = parameters.getApiLevel().getLevel();
-    assertEquals(runtimeLevel >= AndroidApiLevel.L.getLevel(), hasCatchHandler);
-
+    // The catch handler does not exist in ClassWithCatchNonExisting.methodWithCatch thus we assign
+    // UNKNOWN api level. As a result we do not inline.
+    assertFalse(hasCatchHandler);
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldTypeStrengtheningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldTypeStrengtheningTest.java
new file mode 100644
index 0000000..bf1ec2e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldTypeStrengtheningTest.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.membervaluepropagation.fields;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class FieldTypeStrengtheningTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+
+              FieldSubject fieldSubject = mainClassSubject.uniqueFieldWithName("f");
+              assertEquals(
+                  aClassSubject.getFinalName(), fieldSubject.getField().getType().getTypeName());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A");
+  }
+
+  static class Main {
+
+    static Object f;
+
+    public static void main(String[] args) {
+      setField();
+      printField();
+    }
+
+    @NeverInline
+    static void setField() {
+      f = new A();
+    }
+
+    @NeverInline
+    static void printField() {
+      System.out.println(f);
+    }
+  }
+
+  public static class A {
+
+    @Override
+    public String toString() {
+      return "A";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index 2d2611f..05758fd 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_3_72;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_5_0;
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLIN_DEV;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
@@ -36,6 +37,7 @@
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
+import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -62,6 +64,8 @@
     // SAM interfaces lambdas are implemented by invoke dynamic in kotlin 1.5 unlike 1.4 where a
     // class is generated for each. In CF we leave invokeDynamic but for DEX we desugar the classes
     // and merge them.
+    // TODO(b/208816049): Fix test.
+    Assume.assumeTrue(kotlinParameters.getCompiler().isNot(KOTLIN_DEV));
     boolean hasKotlinCGeneratedLambdaClasses = kotlinParameters.isOlderThan(KOTLINC_1_5_0);
     String mainClassName = "class_inliner_lambda_j_style.MainKt";
     runTest(
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java
index 03d1aa2..d426170 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java
@@ -27,10 +27,7 @@
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withDexRuntimes().withAllApiLevels().build(),
-        getKotlinTestParameters()
-            .withAllCompilers()
-            .withTargetVersion(KotlinTargetVersion.JAVA_6)
-            .build());
+        getKotlinTestParameters().withAllCompilersAndTargetVersions().build());
   }
 
   public KotlinLambdaMergingDebugTest(
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java
index 1dae299..39d2473 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java
@@ -99,13 +99,17 @@
             .assertAllWarningMessagesMatch(
                 equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
             .writeToZip();
+    boolean expectingCompilationError = kotlinParameters.isOlderThanMinSupported() && !keepUnit;
     Path output =
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(expectingCompilationError);
+    if (expectingCompilationError) {
+      return;
+    }
     final JvmTestRunResult runResult =
         testForJvm()
             .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java
index 440a8d5..5739e90 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java
@@ -135,7 +135,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar(), libJar)
         .addProgramFiles(output)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java
index 5437d0d..b2e2631 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java
@@ -84,7 +84,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/anonymous_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
         .addClasspath(main)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDelegatedPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDelegatedPropertyTest.java
index 994a011..d3e2677 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDelegatedPropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDelegatedPropertyTest.java
@@ -1,17 +1,28 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.kotlin.KotlinMetadataWriter;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.nio.file.Path;
 import java.util.Collection;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -19,21 +30,25 @@
 @RunWith(Parameterized.class)
 public class MetadataRewriteDelegatedPropertyTest extends KotlinMetadataTestBase {
 
+  private static final String PKG_LIB = PKG + ".delegated_property_lib";
   private static final String PKG_APP = PKG + ".delegated_property_app";
-  private static final String EXPECTED_MAIN =
+  private static final String EXPECTED =
       StringUtils.lines(
-          "Initial string has been read in CustomDelegate from 'x'",
-          "Initial string has been read in CustomDelegate from 'x'",
-          "New value has been read in CustomDelegate from 'x'",
-          "New value has been read in CustomDelegate from 'x'",
-          "null",
-          "New value has been read in CustomDelegate from 'x'");
+          "foobar",
+          "var com.android.tools.r8.kotlin.metadata.delegated_property_lib.MyDelegatedProperty.oldName:"
+              + " kotlin.String");
+
+  private static final KotlinCompilerVersion MIN_SUPPORTED_KOTLIN_VERSION = KOTLINC_1_4_20;
 
   @Parameterized.Parameters(name = "{0}, {1}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withCfRuntimes().build(),
-        getKotlinTestParameters().withAllCompilersAndTargetVersions().build());
+        getKotlinTestParameters()
+            .withOldCompilersStartingFrom(MIN_SUPPORTED_KOTLIN_VERSION)
+            .withCompilersStartingFromIncluding(MIN_SUPPORTED_KOTLIN_VERSION)
+            .withAllTargetVersions()
+            .build());
   }
 
   public MetadataRewriteDelegatedPropertyTest(
@@ -43,17 +58,26 @@
   }
 
   private final TestParameters parameters;
-  private static final KotlinCompileMemoizer jars =
+
+  private static final KotlinCompileMemoizer libJars =
       getCompileMemoizer(
-          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"));
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"));
 
   @Test
   public void smokeTest() throws Exception {
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
+    Path output =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(libJar)
+            .addSourceFiles(
+                getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
+            .setOutputPath(temp.newFolder().toPath())
+            .compile();
     testForJvm()
-        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar())
-        .addClasspath(jars.getForConfiguration(kotlinc, targetVersion))
+        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar(), libJar)
+        .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
-        .assertSuccessWithOutput(EXPECTED_MAIN);
+        .assertSuccessWithOutput(EXPECTED);
   }
 
   @Test
@@ -64,21 +88,60 @@
                 kotlinc.getKotlinStdlibJar(),
                 kotlinc.getKotlinReflectJar(),
                 kotlinc.getKotlinAnnotationJar())
-            .addProgramFiles(jars.getForConfiguration(kotlinc, targetVersion))
-            .addKeepAllClassesRule()
+            .addKeepClassAndMembersRules(PKG_LIB + ".MyDelegatedProperty")
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
             .compile()
-            .inspect(
-                inspector ->
-                    assertEqualMetadata(
-                        new CodeInspector(jars.getForConfiguration(kotlinc, targetVersion)),
-                        inspector,
-                        (addedStrings, addedNonInitStrings) -> {}))
+            .inspect(this::inspectMetadata)
             .writeToZip();
+    Path main =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(outputJar)
+            .addSourceFiles(
+                getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
+            .setOutputPath(temp.newFolder().toPath())
+            .compile();
     testForJvm()
-        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar())
-        .addClasspath(outputJar)
+        .addRunClasspathFiles(
+            kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar(), outputJar)
+        .addClasspath(main)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
-        .assertSuccessWithOutput(EXPECTED_MAIN);
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testInsufficientMetadataForLib() throws Exception {
+    Path outputJar =
+        testForR8(parameters.getBackend())
+            .addClasspathFiles(
+                kotlinc.getKotlinStdlibJar(),
+                kotlinc.getKotlinReflectJar(),
+                kotlinc.getKotlinAnnotationJar())
+            .addKeepClassAndMembersRules(PKG_LIB + ".MyDelegatedProperty")
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
+            .compile()
+            .writeToZip();
+    ProcessResult compileResult =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(outputJar)
+            .addSourceFiles(
+                getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
+            .setOutputPath(temp.newFolder().toPath())
+            .compileRaw();
+    Assert.assertEquals(1, compileResult.exitCode);
+    assertThat(
+        compileResult.stderr,
+        containsString(
+            "unsupported [reference to the synthetic extension property for a Java get/set"
+                + " method]"));
+  }
+
+  private void inspectMetadata(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz(PKG_LIB + ".MyDelegatedProperty");
+    assertThat(clazz, isPresent());
+    KotlinClassMetadata kotlinClassMetadata = clazz.getKotlinClassMetadata();
+    Assert.assertNotNull(kotlinClassMetadata);
+    String metadataAsString = KotlinMetadataWriter.kotlinMetadataToString("", kotlinClassMetadata);
+    assertThat(metadataAsString, containsString("syntheticMethodForDelegate:"));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java
index 15a3391..e45b633 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java
@@ -21,11 +21,9 @@
 import com.android.tools.r8.utils.codeinspector.KmPropertySubject;
 import com.android.tools.r8.utils.codeinspector.KmTypeProjectionSubject;
 import com.android.tools.r8.utils.codeinspector.KmTypeSubject;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
 import kotlinx.metadata.KmFlexibleTypeUpperBound;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -96,7 +94,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/flexible_upper_bound_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar(), libJar)
         .addClasspath(main)
@@ -104,7 +105,7 @@
         .assertSuccessWithOutput(EXPECTED);
   }
 
-  private void inspect(CodeInspector inspector) throws IOException, ExecutionException {
+  private void inspect(CodeInspector inspector) {
     // We are checking that A is renamed, and that the flexible upper bound information is
     // reflecting that.
     ClassSubject a = inspector.clazz(PKG_LIB + ".A");
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java
index 8b9e4c1..2b316c9 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java
@@ -104,7 +104,10 @@
             .addClasspathFiles(baseLibJar, libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/classpath_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), baseLibJar, libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java
index dc4535c..eae75ac 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java
@@ -144,7 +144,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/companion_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java
index b1001e1..a03354e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java
@@ -157,7 +157,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java
index 7f0fc70..67401c2 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java
@@ -162,7 +162,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_property_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java
index 001f000..10afff3 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java
@@ -158,7 +158,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java
index 2707bc2..fb2e952 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java
@@ -98,7 +98,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/vararg_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java
index 97ec9f0..cbbbbb0 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java
@@ -93,7 +93,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/nested_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java
index 6d69d26..557562c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java
@@ -87,7 +87,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/parametertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java
index a29d2ec..7e12a9f 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java
@@ -95,7 +95,10 @@
             .addSourceFiles(
                 getKotlinFileInTest(PKG_PREFIX + "/fragile_property_only_getter", "getter_user"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
@@ -192,7 +195,10 @@
             .addSourceFiles(
                 getKotlinFileInTest(PKG_PREFIX + "/fragile_property_only_setter", "setter_user"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java
index 06ed6a6..2a69c5a 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java
@@ -85,7 +85,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/propertytype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java
index 59d626b..84ee3ec 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java
@@ -87,7 +87,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/returntype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java
index 7aa3952..afe617a 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java
@@ -88,7 +88,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/supertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
@@ -135,7 +138,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/supertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java
index 8482191..cfc0ea8 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java
@@ -138,7 +138,10 @@
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/typealias_app", "main"))
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
 
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar(), libJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java
index 18fce95..880bb14 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java
@@ -140,7 +140,10 @@
         kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/typeargument_app", "main"))
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
         .addClasspath(mainJar)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassIncludeDescriptorClassesTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassIncludeDescriptorClassesTest.java
new file mode 100644
index 0000000..ac394af
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassIncludeDescriptorClassesTest.java
@@ -0,0 +1,112 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata;
+
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.*;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
+import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.Matchers;
+import java.nio.file.Path;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MetadataRewriteInlineClassIncludeDescriptorClassesTest extends KotlinMetadataTestBase {
+
+  private final String EXPECTED = StringUtils.lines("Hello World!");
+  private static final KotlinCompilerVersion MIN_SUPPORTED_KOTLIN_VERSION = KOTLINC_1_6_0;
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(),
+        getKotlinTestParameters()
+            .withOldCompilersStartingFrom(MIN_SUPPORTED_KOTLIN_VERSION)
+            .withCompilersStartingFromIncluding(MIN_SUPPORTED_KOTLIN_VERSION)
+            .withAllTargetVersions()
+            .build());
+  }
+
+  public MetadataRewriteInlineClassIncludeDescriptorClassesTest(
+      TestParameters parameters, KotlinTestParameters kotlinParameters) {
+    super(kotlinParameters);
+    this.parameters = parameters;
+  }
+
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(
+          getKotlinFileInTest(PKG_PREFIX + "/inline_class_fun_descriptor_classes_lib", "lib"),
+          getKotlinFileInTest(
+              PKG_PREFIX + "/inline_class_fun_descriptor_classes_lib", "keepForApi"));
+  private final TestParameters parameters;
+
+  @Test
+  public void smokeTest() throws Exception {
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
+    Path output =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(libJar)
+            .addSourceFiles(
+                getKotlinFileInTest(
+                    PKG_PREFIX + "/inline_class_fun_descriptor_classes_app", "main"))
+            .setOutputPath(temp.newFolder().toPath())
+            .compile();
+    testForJvm()
+        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
+        .addClasspath(output)
+        .run(parameters.getRuntime(), PKG + ".inline_class_fun_descriptor_classes_app.MainKt")
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testMetadataForLib() throws Exception {
+    Path libJar =
+        testForR8(parameters.getBackend())
+            .addClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinAnnotationJar())
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
+            .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+            // kotlinc will generate a method for the unboxed type on the form login-XXXXX(String).
+            // We define an annotation that specify we keep the method and descriptor classes.
+            .addKeepRules(
+                "-keepclasseswithmembers class * { @"
+                    + PKG
+                    + ".inline_class_fun_descriptor_classes_lib.KeepForApi *; }")
+            .addKeepRules(
+                "-keepclassmembers,includedescriptorclasses class * { @"
+                    + PKG
+                    + ".inline_class_fun_descriptor_classes_lib.KeepForApi *; }")
+            .compile()
+            .inspect(
+                inspector -> {
+                  // TODO(b/208209210): Perhaps this should be kept.
+                  assertThat(
+                      inspector.clazz(
+                          PKG + ".inline_class_fun_descriptor_classes_lib.KeepForApi.Password"),
+                      not(Matchers.isPresent()));
+                })
+            .writeToZip();
+    ProcessResult kotlinCompileAppResult =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(libJar)
+            .addSourceFiles(
+                getKotlinFileInTest(
+                    PKG_PREFIX + "/inline_class_fun_descriptor_classes_app", "main"))
+            .setOutputPath(temp.newFolder().toPath())
+            .compileRaw();
+    assertEquals(1, kotlinCompileAppResult.exitCode);
+    assertThat(kotlinCompileAppResult.stderr, containsString("unresolved reference: Password"));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassTest.java
new file mode 100644
index 0000000..af22ae1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlineClassTest.java
@@ -0,0 +1,121 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata;
+
+import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_6_0;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
+import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.kotlin.KotlinMetadataWriter;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import junit.framework.TestCase;
+import kotlinx.metadata.jvm.KotlinClassHeader;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MetadataRewriteInlineClassTest extends KotlinMetadataTestBase {
+
+  private final String EXPECTED = StringUtils.lines("Password(s=Hello World!)");
+  private final String passwordTypeName = PKG + ".inline_class_lib.Password";
+  private static final KotlinCompilerVersion MIN_SUPPORTED_KOTLIN_VERSION = KOTLINC_1_6_0;
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(),
+        getKotlinTestParameters()
+            .withOldCompilersStartingFrom(MIN_SUPPORTED_KOTLIN_VERSION)
+            .withCompilersStartingFromIncluding(MIN_SUPPORTED_KOTLIN_VERSION)
+            .withAllTargetVersions()
+            .build());
+  }
+
+  public MetadataRewriteInlineClassTest(
+      TestParameters parameters, KotlinTestParameters kotlinParameters) {
+    super(kotlinParameters);
+    this.parameters = parameters;
+  }
+
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/inline_class_lib", "lib"));
+  private final TestParameters parameters;
+
+  @Test
+  public void smokeTest() throws Exception {
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
+    Path output =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(libJar)
+            .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/inline_class_app", "main"))
+            .setOutputPath(temp.newFolder().toPath())
+            .compile();
+    testForJvm()
+        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
+        .addClasspath(output)
+        .run(parameters.getRuntime(), PKG + ".inline_class_app.MainKt")
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testMetadataForLib() throws Exception {
+    Path libJar =
+        testForR8(parameters.getBackend())
+            .addClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinAnnotationJar())
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
+            .addKeepClassAndMembersRules(passwordTypeName)
+            // kotlinc will generate a method for the unboxed type on the form login-XXXXX(String).
+            // Ideally, this should be targeted by annotation instead.
+            .addKeepRules(
+                "-keep class " + PKG + ".inline_class_lib.LibKt { *** login-*(java.lang.String); }")
+            .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+            .compile()
+            .inspect(this::inspect)
+            .writeToZip();
+    Path main =
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(libJar)
+            .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/inline_class_app", "main"))
+            .setOutputPath(temp.newFolder().toPath())
+            .compile();
+    testForJvm()
+        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar(), libJar)
+        .addClasspath(main)
+        .run(parameters.getRuntime(), PKG + ".inline_class_app.MainKt")
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  private void inspect(CodeInspector inspector) throws IOException {
+    CodeInspector stdLibInspector =
+        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion));
+    ClassSubject clazzSubject = stdLibInspector.clazz(passwordTypeName);
+    ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
+    assertThat(r8Clazz, isPresent());
+    KotlinClassMetadata originalMetadata = clazzSubject.getKotlinClassMetadata();
+    KotlinClassMetadata rewrittenMetadata = r8Clazz.getKotlinClassMetadata();
+    TestCase.assertNotNull(rewrittenMetadata);
+    KotlinClassHeader originalHeader = originalMetadata.getHeader();
+    KotlinClassHeader rewrittenHeader = rewrittenMetadata.getHeader();
+    TestCase.assertEquals(originalHeader.getKind(), rewrittenHeader.getKind());
+    TestCase.assertEquals(originalHeader.getPackageName(), rewrittenHeader.getPackageName());
+    Assert.assertArrayEquals(originalHeader.getData1(), rewrittenHeader.getData1());
+    Assert.assertArrayEquals(originalHeader.getData2(), rewrittenHeader.getData2());
+    String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
+    String actual = KotlinMetadataWriter.kotlinMetadataToString("", rewrittenMetadata);
+    TestCase.assertEquals(expected, actual);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteLocalDelegatedPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteLocalDelegatedPropertyTest.java
new file mode 100644
index 0000000..2a7ee54
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteLocalDelegatedPropertyTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2020, 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.kotlin.metadata;
+
+import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.nio.file.Path;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MetadataRewriteLocalDelegatedPropertyTest extends KotlinMetadataTestBase {
+
+  private static final String PKG_APP = PKG + ".local_delegated_property_app";
+  private static final String EXPECTED_MAIN =
+      StringUtils.lines(
+          "Initial string has been read in CustomDelegate from 'x'",
+          "Initial string has been read in CustomDelegate from 'x'",
+          "New value has been read in CustomDelegate from 'x'",
+          "New value has been read in CustomDelegate from 'x'",
+          "null",
+          "New value has been read in CustomDelegate from 'x'");
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(),
+        getKotlinTestParameters().withAllCompilersAndTargetVersions().build());
+  }
+
+  public MetadataRewriteLocalDelegatedPropertyTest(
+      TestParameters parameters, KotlinTestParameters kotlinParameters) {
+    super(kotlinParameters);
+    this.parameters = parameters;
+  }
+
+  private final TestParameters parameters;
+  private static final KotlinCompileMemoizer jars =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"));
+
+  @Test
+  public void smokeTest() throws Exception {
+    testForJvm()
+        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar())
+        .addClasspath(jars.getForConfiguration(kotlinc, targetVersion))
+        .run(parameters.getRuntime(), PKG_APP + ".MainKt")
+        .assertSuccessWithOutput(EXPECTED_MAIN);
+  }
+
+  @Test
+  public void testMetadataForLib() throws Exception {
+    Path outputJar =
+        testForR8(parameters.getBackend())
+            .addClasspathFiles(
+                kotlinc.getKotlinStdlibJar(),
+                kotlinc.getKotlinReflectJar(),
+                kotlinc.getKotlinAnnotationJar())
+            .addProgramFiles(jars.getForConfiguration(kotlinc, targetVersion))
+            .addKeepAllClassesRule()
+            .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+            .compile()
+            .inspect(
+                inspector ->
+                    assertEqualMetadata(
+                        new CodeInspector(jars.getForConfiguration(kotlinc, targetVersion)),
+                        inspector,
+                        (addedStrings, addedNonInitStrings) -> {}))
+            .writeToZip();
+    testForJvm()
+        .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinReflectJar())
+        .addClasspath(outputJar)
+        .run(parameters.getRuntime(), PKG_APP + ".MainKt")
+        .assertSuccessWithOutput(EXPECTED_MAIN);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java
index 0cf8903..b46dc08 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java
@@ -89,7 +89,10 @@
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
-            .compile();
+            .compile(kotlinParameters.isOlderThanMinSupported());
+    if (kotlinParameters.isOlderThanMinSupported()) {
+      return;
+    }
     testForJvm()
         .addRunClasspathFiles(kotlinc.getKotlinStdlibJar(), libJar)
         .addProgramFiles(output)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteValueClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteValueClassTest.java
index 29fa8ec..6aecc85 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteValueClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteValueClassTest.java
@@ -11,6 +11,7 @@
 import static org.hamcrest.core.StringContains.containsString;
 import static org.junit.Assert.assertNull;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.kotlin.KotlinMetadataWriter;
@@ -37,13 +38,15 @@
   private static final String PKG_LIB = PKG + ".value_class_lib";
   private static final String PKG_APP = PKG + ".value_class_app";
   private final TestParameters parameters;
+  private static final KotlinCompilerVersion MIN_SUPPORTED_KOTLIN_VERSION = KOTLINC_1_5_0;
 
   @Parameterized.Parameters(name = "{0}, {1}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withCfRuntimes().build(),
         getKotlinTestParameters()
-            .withCompilersStartingFromIncluding(KOTLINC_1_5_0)
+            .withOldCompilersStartingFrom(MIN_SUPPORTED_KOTLIN_VERSION)
+            .withCompilersStartingFromIncluding(MIN_SUPPORTED_KOTLIN_VERSION)
             .withTargetVersion(JAVA_8)
             .build());
   }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/delegated_property_app/main.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/delegated_property_app/main.kt
index a35754f..43a9e2e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/delegated_property_app/main.kt
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/delegated_property_app/main.kt
@@ -1,59 +1,14 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
+
 package com.android.tools.r8.kotlin.metadata.delegated_property_app
 
-import kotlin.reflect.KMutableProperty1
-import kotlin.reflect.KProperty
-import kotlin.reflect.full.declaredMemberProperties
-import kotlin.reflect.jvm.isAccessible
-
-class Resource(private var s : String = "Initial string") {
-
-  override fun toString(): String {
-    return s;
-  }
-}
-
-object CustomDelegate {
-
-  private var resource : Resource = Resource()
-
-  operator fun getValue(thisRef: Any?, property: KProperty<*>): Resource {
-    println("$resource has been read in CustomDelegate from '${property.name}'")
-    return resource;
-  }
-
-  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
-    println("$value has been assigned to '${property.name}'")
-    this.resource = resource;
-  }
-}
-
-open class Base {
-
-  fun doSomethingOnBarRef() : Resource {
-    var x by CustomDelegate
-    val propRef = x.javaClass.kotlin.declaredMemberProperties.first() as KMutableProperty1<Resource, String>
-    propRef.isAccessible = true
-    propRef.set(x, "New value")
-    propRef.get(x)
-    // Getting the delegate is not yet supported and will return null. We are printing the value
-    // allowing us to observe if the behavior changes.
-    println(propRef.getDelegate(x))
-    return x
-  }
-}
-
-object Impl : Base() {
-  operator fun invoke(): Impl {
-    return this
-  }
-}
-
+import com.android.tools.r8.kotlin.metadata.delegated_property_lib.MyDelegatedProperty
 
 fun main() {
-  val impl = Impl()
-  impl.doSomethingOnBarRef()
+  val myDelegatedProperty = MyDelegatedProperty()
+  myDelegatedProperty.oldName = "foobar";
+  println(myDelegatedProperty.newName)
+  println(myDelegatedProperty::oldName.toString())
 }
-
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/delegated_property_lib/lib.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/delegated_property_lib/lib.kt
new file mode 100644
index 0000000..da40ce5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/delegated_property_lib/lib.kt
@@ -0,0 +1,10 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata.delegated_property_lib
+
+class MyDelegatedProperty {
+  var newName: String = "Hello World!"
+  var oldName: String by this::newName
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_app/main.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_app/main.kt
new file mode 100644
index 0000000..28cecd5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_app/main.kt
@@ -0,0 +1,12 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata.inline_class_app
+
+import com.android.tools.r8.kotlin.metadata.inline_class_lib.Password
+import com.android.tools.r8.kotlin.metadata.inline_class_lib.login
+
+fun main() {
+  login(Password("Hello World!"))
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_app/main.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_app/main.kt
new file mode 100644
index 0000000..a9d5023
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_app/main.kt
@@ -0,0 +1,13 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata.inline_class_fun_descriptor_classes_app
+
+import com.android.tools.r8.kotlin.metadata.inline_class_fun_descriptor_classes_lib.create
+import com.android.tools.r8.kotlin.metadata.inline_class_fun_descriptor_classes_lib.Password
+import com.android.tools.r8.kotlin.metadata.inline_class_fun_descriptor_classes_lib.login
+
+fun main() {
+  login(create("Hello World!"))
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_lib/keepForApi.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_lib/keepForApi.kt
new file mode 100644
index 0000000..1e0ec5a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_lib/keepForApi.kt
@@ -0,0 +1,11 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata.inline_class_fun_descriptor_classes_lib
+
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeepForApi {
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_lib/lib.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_lib/lib.kt
new file mode 100644
index 0000000..8c902e7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_lib/lib.kt
@@ -0,0 +1,18 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata.inline_class_fun_descriptor_classes_lib
+
+@JvmInline
+value class Password(val s: String)
+
+@KeepForApi
+fun create(pw : String) : Password {
+  return Password(pw)
+}
+
+@KeepForApi
+fun login(pw : Password) {
+  println(pw.s)
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_lib/lib.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_lib/lib.kt
new file mode 100644
index 0000000..56bc18f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_lib/lib.kt
@@ -0,0 +1,12 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata.inline_class_lib
+
+@JvmInline
+value class Password(private val s: String)
+
+fun login(pw : Password) {
+  println(pw)
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/local_delegated_property_app/main.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/local_delegated_property_app/main.kt
new file mode 100644
index 0000000..b8959e8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/local_delegated_property_app/main.kt
@@ -0,0 +1,59 @@
+// Copyright (c) 2020, 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.kotlin.metadata.local_delegated_property_app
+
+import kotlin.reflect.KMutableProperty1
+import kotlin.reflect.KProperty
+import kotlin.reflect.full.declaredMemberProperties
+import kotlin.reflect.jvm.isAccessible
+
+class Resource(private var s : String = "Initial string") {
+
+  override fun toString(): String {
+    return s;
+  }
+}
+
+object CustomDelegate {
+
+  private var resource : Resource = Resource()
+
+  operator fun getValue(thisRef: Any?, property: KProperty<*>): Resource {
+    println("$resource has been read in CustomDelegate from '${property.name}'")
+    return resource;
+  }
+
+  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
+    println("$value has been assigned to '${property.name}'")
+    this.resource = resource;
+  }
+}
+
+open class Base {
+
+  fun doSomethingOnBarRef() : Resource {
+    var x by CustomDelegate
+    val propRef = x.javaClass.kotlin.declaredMemberProperties.first() as KMutableProperty1<Resource, String>
+    propRef.isAccessible = true
+    propRef.set(x, "New value")
+    propRef.get(x)
+    // Getting the delegate is not yet supported and will return null. We are printing the value
+    // allowing us to observe if the behavior changes.
+    println(propRef.getDelegate(x))
+    return x
+  }
+}
+
+object Impl : Base() {
+  operator fun invoke(): Impl {
+    return this
+  }
+}
+
+
+fun main() {
+  val impl = Impl()
+  impl.doSomethingOnBarRef()
+}
+
diff --git a/src/test/java/com/android/tools/r8/naming/OverloadedReservedFieldNamingTest.java b/src/test/java/com/android/tools/r8/naming/OverloadedReservedFieldNamingTest.java
index 2e211ad..cda6322 100644
--- a/src/test/java/com/android/tools/r8/naming/OverloadedReservedFieldNamingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/OverloadedReservedFieldNamingTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.NoFieldTypeStrengthening;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.references.Reference;
@@ -30,7 +31,8 @@
 
   @Parameterized.Parameters(name = "{1}, overload aggressively: {0}")
   public static List<Object[]> data() {
-    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public OverloadedReservedFieldNamingTest(
@@ -47,7 +49,8 @@
         .addKeepRules(
             "-keep class " + A.class.getTypeName() + " { boolean a; }",
             overloadAggressively ? "-overloadaggressively" : "")
-        .setMinApi(parameters.getRuntime())
+        .enableNoFieldTypeStrengtheningAnnotations()
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::verifyAggressiveOverloading)
         .run(parameters.getRuntime(), TestClass.class)
@@ -82,6 +85,8 @@
 
     static final boolean a = System.currentTimeMillis() >= 0;
     static final String hello = System.currentTimeMillis() >= 0 ? "Hello" : null;
+
+    @NoFieldTypeStrengthening
     static final Object world = System.currentTimeMillis() >= 0 ? " world!" : null;
 
     @Override
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateFieldTypeTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateFieldTypeTest.java
index 543dbf8..6faa4f7 100644
--- a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateFieldTypeTest.java
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateFieldTypeTest.java
@@ -9,7 +9,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.NeverPropagateValue;
+import com.android.tools.r8.NoFieldTypeStrengthening;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -33,6 +33,7 @@
         .addKeepClassRules(NonPublicKeptClass.class)
         .apply(this::configureRepackaging)
         .enableInliningAnnotations()
+        .enableNoFieldTypeStrengtheningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::inspect)
@@ -63,6 +64,7 @@
 
   public static class IneligibleForRepackaging {
 
+    @NoFieldTypeStrengthening
     private static NonPublicKeptClass FIELD =
         System.currentTimeMillis() > 0 ? new PublicSubClass() : null;
 
diff --git a/src/test/java/com/android/tools/r8/softverification/FoundClass.java b/src/test/java/com/android/tools/r8/softverification/FoundClass.java
index cc25389..2165828 100644
--- a/src/test/java/com/android/tools/r8/softverification/FoundClass.java
+++ b/src/test/java/com/android/tools/r8/softverification/FoundClass.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.softverification;
 
-public class FoundClass {
+public class FoundClass extends RuntimeException {
 
   public static int staticField = 42;
 
diff --git a/src/test/java/com/android/tools/r8/softverification/MissingClass.java b/src/test/java/com/android/tools/r8/softverification/MissingClass.java
index d653af7..189ba2b 100644
--- a/src/test/java/com/android/tools/r8/softverification/MissingClass.java
+++ b/src/test/java/com/android/tools/r8/softverification/MissingClass.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.softverification;
 
-public class MissingClass {
+public class MissingClass extends RuntimeException {
 
   public static int staticField = 42;
 
diff --git a/src/test/java/com/android/tools/r8/softverification/MissingMember.java b/src/test/java/com/android/tools/r8/softverification/MissingMember.java
index 9fee087..8d9a2de 100644
--- a/src/test/java/com/android/tools/r8/softverification/MissingMember.java
+++ b/src/test/java/com/android/tools/r8/softverification/MissingMember.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.softverification;
 
-public class MissingMember {
+public class MissingMember extends RuntimeException {
 
   public static int staticField = 42;
 
diff --git a/src/test/java/com/android/tools/r8/softverification/MissingSuperType.java b/src/test/java/com/android/tools/r8/softverification/MissingSuperType.java
new file mode 100644
index 0000000..e77ea01
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/softverification/MissingSuperType.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.softverification;
+
+public class MissingSuperType extends MissingClass {
+
+  public static int staticField = 42;
+
+  public int instanceField = 42;
+
+  public static void staticMethod() {
+    System.out.println("MissingSuperType::staticMethod");
+  }
+
+  @Override
+  public void instanceMethod() {
+    System.out.println("MissingSuperType::instanceMethod");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/softverification/TestHashCode.java b/src/test/java/com/android/tools/r8/softverification/TestHashCode.java
new file mode 100644
index 0000000..d856b71
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/softverification/TestHashCode.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.softverification;
+
+public class TestHashCode {
+
+  public static String run() {
+    return run(null);
+  }
+
+  public static String run(MissingClass missingClass) {
+    if (System.currentTimeMillis() == 0) {
+      missingClass.hashCode();
+    }
+    if (System.currentTimeMillis() == 0) {
+      missingClass.hashCode();
+    }
+    String currentString = "foobar";
+    for (int i = 0; i < 10; i++) {
+      currentString = "foobar" + (i + currentString.length());
+    }
+    return currentString;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/softverification/TestRunner.java b/src/test/java/com/android/tools/r8/softverification/TestRunner.java
index 6baef2a..dbe5ab1 100644
--- a/src/test/java/com/android/tools/r8/softverification/TestRunner.java
+++ b/src/test/java/com/android/tools/r8/softverification/TestRunner.java
@@ -47,9 +47,15 @@
     measure.start("InstanceField");
     TestInstanceField.run();
     sb.append(measure.stop());
+    measure.start("HashCode");
+    TestHashCode.run();
+    sb.append(measure.stop());
     measure.start("InstanceMethod");
     TestInstanceMethod.run();
     sb.append(measure.stop());
+    measure.start("TryCatch");
+    TestTryCatch.run();
+    sb.append(measure.stop());
     return sb.toString();
   }
 
diff --git a/src/test/java/com/android/tools/r8/softverification/TestRunnerBuilder.java b/src/test/java/com/android/tools/r8/softverification/TestRunnerBuilder.java
index 375f03f..c3f91db 100644
--- a/src/test/java/com/android/tools/r8/softverification/TestRunnerBuilder.java
+++ b/src/test/java/com/android/tools/r8/softverification/TestRunnerBuilder.java
@@ -74,8 +74,11 @@
 
   private static final Path ANDROID_STUDIO_LIB_PATH = Paths.get("PATH_TO_PROJECT/libs/library.jar");
 
-  private static final int COUNT = 1100;
+  private static final int COUNT = 800;
 
+  private static final List<Class<?>> referenceClasses =
+      ImmutableList.of(
+          MissingClass.class, MissingMember.class, FoundClass.class, MissingSuperType.class);
   private static final List<Class<?>> testClasses =
       ImmutableList.of(
           TestCheckCast.class,
@@ -85,7 +88,9 @@
           TestStaticField.class,
           TestStaticMethod.class,
           TestInstanceField.class,
-          TestInstanceMethod.class);
+          TestInstanceMethod.class,
+          TestHashCode.class,
+          TestTryCatch.class);
   private static final Collection<String> testClassBinaryNames =
       ImmutableSet.copyOf(ListUtils.map(testClasses, TestBase::binaryName));
 
@@ -93,8 +98,7 @@
     ZipBuilder builder = ZipBuilder.builder(path);
     builder.addFilesRelative(
         ToolHelper.getClassPathForTests(), ToolHelper.getClassFileForTestClass(Measure.class));
-    for (Class<?> clazz :
-        ImmutableList.of(MissingClass.class, MissingMember.class, FoundClass.class)) {
+    for (Class<?> clazz : referenceClasses) {
       String postFix = clazz.getSimpleName();
       int classCounter = 0;
       for (int i = 0; i < COUNT; i++) {
@@ -168,6 +172,11 @@
             .replaceClassDescriptorInMethodInstructions(
                 descriptor(MissingClass.class),
                 getDescriptorFromClassBinaryName(referenceBinaryName))
+            .transformTryCatchBlock(
+                "run",
+                (start, end, handler, type, visitor) -> {
+                  visitor.visitTryCatchBlock(start, end, handler, referenceBinaryName);
+                })
             .transform());
   }
 
diff --git a/src/test/java/com/android/tools/r8/softverification/TestTryCatch.java b/src/test/java/com/android/tools/r8/softverification/TestTryCatch.java
new file mode 100644
index 0000000..4be92a3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/softverification/TestTryCatch.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.softverification;
+
+public class TestTryCatch {
+
+  public static Object getObject() {
+    return new Object();
+  }
+
+  public static String run() {
+    try {
+      getObject();
+    } catch (MissingClass e) {
+      throw new RuntimeException("Foo");
+    }
+    String currentString = "foobar";
+    for (int i = 0; i < 10; i++) {
+      currentString = "foobar" + (i + currentString.length());
+    }
+    return currentString;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
index 689fce0..69b8232 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
@@ -71,8 +71,17 @@
 
   public HorizontallyMergedClassesInspector applyIf(
       boolean condition, ThrowableConsumer<HorizontallyMergedClassesInspector> consumer) {
+    return applyIf(condition, consumer, ThrowableConsumer.empty());
+  }
+
+  public HorizontallyMergedClassesInspector applyIf(
+      boolean condition,
+      ThrowableConsumer<HorizontallyMergedClassesInspector> thenConsumer,
+      ThrowableConsumer<HorizontallyMergedClassesInspector> elseConsumer) {
     if (condition) {
-      consumer.acceptWithRuntimeException(this);
+      thenConsumer.acceptWithRuntimeException(this);
+    } else {
+      elseConsumer.acceptWithRuntimeException(this);
     }
     return this;
   }
diff --git a/tools/test.py b/tools/test.py
index f5ce159..041f17b 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -287,9 +287,9 @@
   if options.print_obfuscated_stacktraces:
     gradle_args.append('-Pprint_obfuscated_stacktraces')
   if options.kotlin_compiler_old:
-    gradle_args.append('-Dcom.android.tools.r8.kotlincompilerold=1')
+    gradle_args.append('-Pkotlin_compiler_old')
   if options.kotlin_compiler_dev:
-    gradle_args.append('-Dcom.android.tools.r8.kotlincompilerdev=1')
+    gradle_args.append('-Pkotlin_compiler_dev')
     download_kotlin_dev.download_newest()
   if os.name == 'nt':
     # temporary hack