Reland "Introduce a structure of preservation requirements for program items."

The reverts and merges the previous CL. To fix the issue for the revert,
keep-info bottom is used for non-program classes.

Bug: 156715504
Bug: 157538235
Change-Id: I6717e897bc8512ad3d2e5a4b3368a13de677ce5f
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index 95d4085..22aa578 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
@@ -14,9 +15,11 @@
 import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Deque;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Supplier;
@@ -256,6 +259,14 @@
     return true;
   }
 
+  public <T extends DexReference> boolean assertPinnedNotModified(KeepInfoCollection keepInfo) {
+    List<DexReference> pinnedItems = new ArrayList<>();
+    keepInfo.forEachPinnedType(pinnedItems::add);
+    keepInfo.forEachPinnedMethod(pinnedItems::add);
+    keepInfo.forEachPinnedField(pinnedItems::add);
+    return assertReferencesNotModified(pinnedItems);
+  }
+
   public <T extends DexReference> boolean assertReferencesNotModified(Iterable<T> references) {
     for (DexReference reference : references) {
       if (reference.isDexField()) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
index c64dff4..667c1ff 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
@@ -9,7 +9,6 @@
 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.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldAccessInfo;
 import com.android.tools.r8.graph.FieldAccessInfoCollection;
@@ -26,6 +25,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.DefaultTreePrunerConfiguration;
 import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.shaking.TreePrunerConfiguration;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
@@ -190,7 +190,7 @@
           resolutionResult.asSuccessfulResolution().getResolutionPair().asProgramField();
       return field != null
           && isDeadProtoExtensionField(
-              field, appInfo.getFieldAccessInfoCollection(), appInfo.getPinnedItems());
+              field, appInfo.getFieldAccessInfoCollection(), appInfo.getKeepInfo());
     }
     return false;
   }
@@ -198,8 +198,8 @@
   public boolean isDeadProtoExtensionField(
       ProgramField field,
       FieldAccessInfoCollection<?> fieldAccessInfoCollection,
-      Set<DexReference> pinnedItems) {
-    if (pinnedItems.contains(field.getReference())) {
+      KeepInfoCollection keepInfo) {
+    if (keepInfo.getFieldInfo(field).isPinned()) {
       return false;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index dcbf623..2daa142 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -54,7 +54,6 @@
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.AppInfoWithLivenessModifier;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -373,13 +372,18 @@
   }
 
   private void updatePinnedItems(Set<DexType> enumsToUnbox) {
-    AppInfoWithLivenessModifier modifier = AppInfoWithLiveness.modifier();
-    for (DexType type : enumsToUnbox) {
-      DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
-      assert !appView.appInfo().isPinned(clazz.type);
-      modifier.removePinnedClassMembers(clazz);
-    }
-    modifier.modify(appView.appInfo());
+    appView
+        .appInfo()
+        .getKeepInfo()
+        .mutate(
+            keepInfo -> {
+              for (DexType type : enumsToUnbox) {
+                DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
+                assert !keepInfo.getClassInfo(clazz).isPinned();
+                clazz.forEachProgramMethod(keepInfo::unpinMethod);
+                clazz.forEachField(field -> keepInfo.unpinField(clazz, field));
+              }
+            });
   }
 
   public void finishAnalysis() {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index b5530c2..ed16561 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -7,17 +7,15 @@
 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.DexItemFactory;
 import com.android.tools.r8.graph.DexMember;
-import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
-import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxer.Reason;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -146,22 +144,17 @@
     // A holder type, for field or method, should block enum unboxing only if the enum type is
     // also kept. This is to allow the keep rule -keepclassmembers to be used on enums while
     // enum unboxing can still be performed.
-    for (DexReference item : appView.appInfo().getPinnedItems()) {
-      if (item.isDexType()) {
-        removePinnedCandidate(item.asDexType());
-      } else if (item.isDexField()) {
-        DexField field = item.asDexField();
-        removePinnedIfNotHolder(field, field.type);
-      } else {
-        assert item.isDexMethod();
-        DexMethod method = item.asDexMethod();
-        DexProto proto = method.proto;
-        removePinnedIfNotHolder(method, proto.returnType);
-        for (DexType parameterType : proto.parameters.values) {
-          removePinnedIfNotHolder(method, parameterType);
-        }
-      }
-    }
+    KeepInfoCollection keepInfo = appView.appInfo().getKeepInfo();
+    keepInfo.forEachPinnedType(this::removePinnedCandidate);
+    keepInfo.forEachPinnedField(field -> removePinnedIfNotHolder(field, field.type));
+    keepInfo.forEachPinnedMethod(
+        method -> {
+          DexProto proto = method.proto;
+          removePinnedIfNotHolder(method, proto.returnType);
+          for (DexType parameterType : proto.parameters.values) {
+            removePinnedIfNotHolder(method, parameterType);
+          }
+        });
   }
 
   private void removePinnedIfNotHolder(DexMember<?, ?> member, DexType type) {
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
index 28928ef..8b503e6 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
@@ -34,9 +34,9 @@
     // We will process kotlin.Metadata even if the type is not present in the program, as long as
     // the annotation will be in the output
     boolean keepMetadata =
-        enqueuer.isPinned(kotlinMetadataType)
-            || enqueuer.isMissing(kotlinMetadataType)
-            || (kotlinMetadataClass != null && kotlinMetadataClass.isNotProgramClass());
+        kotlinMetadataClass == null
+            || kotlinMetadataClass.isNotProgramClass()
+            || enqueuer.isPinned(kotlinMetadataType);
     enqueuer.forAllLiveClasses(
         clazz -> {
           clazz.setKotlinInfo(
diff --git a/src/main/java/com/android/tools/r8/optimize/BridgeHoisting.java b/src/main/java/com/android/tools/r8/optimize/BridgeHoisting.java
index 31e239b..54decce 100644
--- a/src/main/java/com/android/tools/r8/optimize/BridgeHoisting.java
+++ b/src/main/java/com/android/tools/r8/optimize/BridgeHoisting.java
@@ -254,8 +254,9 @@
 
     // Remove all of the bridges in the eligible subclasses.
     for (DexProgramClass subclass : eligibleSubclasses) {
+      assert !appView.appInfo().isPinned(method);
       DexEncodedMethod removed = subclass.removeMethod(method);
-      assert removed != null && !appView.appInfo().isPinned(removed.method);
+      assert removed != null;
     }
   }
 
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 584b84b..02aca6b 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -305,14 +305,15 @@
               if (appView.appInfo().isPinned(ica.getInner())) {
                 return false;
               }
-              if (appView.appInfo().isPinned(ica.getOuter())) {
+              DexType outer = ica.getOuter();
+              if (outer != null && appView.appInfo().isPinned(outer)) {
                 return false;
               }
               if (finalKeepForThisInnerClass && ica.getInner() == clazz.type) {
                 return false;
               }
               if (finalKeepForThisEnclosingClass
-                  && ica.getOuter() == clazz.type
+                  && outer == clazz.type
                   && classesToRetainInnerClassAttributeFor.contains(ica.getInner())) {
                 return false;
               }
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 34fdfe7..4d3b6aa 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -47,11 +47,9 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.PredicateSet;
-import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.Visibility;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
@@ -133,10 +131,14 @@
    * will have been removed from the code.
    */
   public final Set<DexCallSite> callSites;
-  /** Set of all items that have to be kept independent of whether they are used. */
-  final Set<DexReference> pinnedItems;
-  /** Set of kept items that are allowed to be publicized. */
-  final Set<DexReference> allowAccessModification;
+  /** Collection of keep requirements for the program. */
+  private final KeepInfoCollection keepInfo;
+  /**
+   * Set of kept items that are allowed to be publicized.
+   *
+   * <p>TODO(b/156715504): merge into keep info.
+   */
+  private final Set<DexReference> allowAccessModification;
   /** All items with assumemayhavesideeffects rule. */
   public final Map<DexReference, ProguardMemberRule> mayHaveSideEffects;
   /** All items with assumenosideeffects rule. */
@@ -214,7 +216,7 @@
       SortedMap<DexMethod, ProgramMethodSet> directInvokes,
       SortedMap<DexMethod, ProgramMethodSet> staticInvokes,
       Set<DexCallSite> callSites,
-      Set<DexReference> pinnedItems,
+      KeepInfoCollection keepInfo,
       Set<DexReference> allowAccessModification,
       Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
       Map<DexReference, ProguardMemberRule> noSideEffects,
@@ -250,7 +252,7 @@
     this.liveMethods = liveMethods;
     this.fieldAccessInfoCollection = fieldAccessInfoCollection;
     this.objectAllocationInfoCollection = objectAllocationInfoCollection;
-    this.pinnedItems = pinnedItems;
+    this.keepInfo = keepInfo;
     this.allowAccessModification = allowAccessModification;
     this.mayHaveSideEffects = mayHaveSideEffects;
     this.noSideEffects = noSideEffects;
@@ -301,7 +303,7 @@
       SortedMap<DexMethod, ProgramMethodSet> directInvokes,
       SortedMap<DexMethod, ProgramMethodSet> staticInvokes,
       Set<DexCallSite> callSites,
-      Set<DexReference> pinnedItems,
+      KeepInfoCollection keepInfo,
       Set<DexReference> allowAccessModification,
       Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
       Map<DexReference, ProguardMemberRule> noSideEffects,
@@ -337,7 +339,7 @@
     this.liveMethods = liveMethods;
     this.fieldAccessInfoCollection = fieldAccessInfoCollection;
     this.objectAllocationInfoCollection = objectAllocationInfoCollection;
-    this.pinnedItems = pinnedItems;
+    this.keepInfo = keepInfo;
     this.allowAccessModification = allowAccessModification;
     this.mayHaveSideEffects = mayHaveSideEffects;
     this.noSideEffects = noSideEffects;
@@ -389,7 +391,7 @@
         previous.directInvokes,
         previous.staticInvokes,
         previous.callSites,
-        previous.pinnedItems,
+        previous.keepInfo,
         previous.allowAccessModification,
         previous.mayHaveSideEffects,
         previous.noSideEffects,
@@ -440,9 +442,7 @@
         previous.directInvokes,
         previous.staticInvokes,
         previous.callSites,
-        additionalPinnedItems == null
-            ? previous.pinnedItems
-            : SetUtils.newIdentityHashSet(previous.pinnedItems, additionalPinnedItems),
+        extendPinnedItems(previous, additionalPinnedItems),
         previous.allowAccessModification,
         previous.mayHaveSideEffects,
         previous.noSideEffects,
@@ -468,7 +468,44 @@
         previous.constClassReferences,
         previous.initClassReferences);
     copyMetadataFromPrevious(previous);
-    assert removedClasses == null || assertNoItemRemoved(previous.pinnedItems, removedClasses);
+    assert keepInfo.verifyNoneArePinned(removedClasses, previous);
+  }
+
+  private static KeepInfoCollection extendPinnedItems(
+      AppInfoWithLiveness previous, Collection<DexReference> additionalPinnedItems) {
+    if (additionalPinnedItems == null || additionalPinnedItems.isEmpty()) {
+      return previous.keepInfo;
+    }
+    return previous.keepInfo.mutate(
+        collection -> {
+          for (DexReference reference : additionalPinnedItems) {
+            if (reference.isDexType()) {
+              DexProgramClass clazz =
+                  asProgramClassOrNull(previous.definitionFor(reference.asDexType()));
+              if (clazz != null) {
+                collection.pinClass(clazz);
+              }
+            } else if (reference.isDexMethod()) {
+              DexMethod method = reference.asDexMethod();
+              DexProgramClass clazz = asProgramClassOrNull(previous.definitionFor(method.holder));
+              if (clazz != null) {
+                DexEncodedMethod definition = clazz.lookupMethod(method);
+                if (definition != null) {
+                  collection.pinMethod(clazz, definition);
+                }
+              }
+            } else {
+              DexField field = reference.asDexField();
+              DexProgramClass clazz = asProgramClassOrNull(previous.definitionFor(field.holder));
+              if (clazz != null) {
+                DexEncodedField definition = clazz.lookupField(field);
+                if (definition != null) {
+                  collection.pinField(clazz, definition);
+                }
+              }
+            }
+          }
+        });
   }
 
   public AppInfoWithLiveness(
@@ -488,7 +525,7 @@
     this.liveMethods = previous.liveMethods;
     this.fieldAccessInfoCollection = previous.fieldAccessInfoCollection;
     this.objectAllocationInfoCollection = previous.objectAllocationInfoCollection;
-    this.pinnedItems = previous.pinnedItems;
+    this.keepInfo = previous.keepInfo;
     this.allowAccessModification = previous.allowAccessModification;
     this.mayHaveSideEffects = previous.mayHaveSideEffects;
     this.noSideEffects = previous.noSideEffects;
@@ -749,27 +786,6 @@
     singleTargetLookupCache.removeInstantiatedType(clazz.type, this);
   }
 
-  void removePinnedItem(DexReference item) {
-    pinnedItems.remove(item);
-  }
-
-  private boolean assertNoItemRemoved(Collection<DexReference> items, Collection<DexType> types) {
-    Set<DexType> typeSet = ImmutableSet.copyOf(types);
-    for (DexReference item : items) {
-      DexType typeToCheck;
-      if (item.isDexType()) {
-        typeToCheck = item.asDexType();
-      } else if (item.isDexMethod()) {
-        typeToCheck = item.asDexMethod().holder;
-      } else {
-        assert item.isDexField();
-        typeToCheck = item.asDexField().holder;
-      }
-      assert !typeSet.contains(typeToCheck);
-    }
-    return true;
-  }
-
   private boolean isInstantiatedDirectly(DexProgramClass clazz) {
     assert checkIfObsolete();
     DexType type = clazz.type;
@@ -796,7 +812,7 @@
     if (info != null && info.isRead()) {
       return true;
     }
-    return isPinned(field)
+    return keepInfo.isPinned(field, this)
         // Fields in the class that is synthesized by D8/R8 would be used soon.
         || field.holder.isD8R8SynthesizedClassType()
         // For library classes we don't know whether a field is read.
@@ -868,7 +884,7 @@
     return method.getDefinition().hasCode()
         && !method.getDefinition().isLibraryMethodOverride().isPossiblyTrue()
         && !neverReprocess.contains(reference)
-        && !pinnedItems.contains(reference);
+        && !keepInfo.getMethodInfo(method).isPinned();
   }
 
   public boolean mayPropagateValueFor(DexReference reference) {
@@ -919,7 +935,7 @@
 
   public boolean isPinned(DexReference reference) {
     assert checkIfObsolete();
-    return pinnedItems.contains(reference);
+    return keepInfo.isPinned(reference, this);
   }
 
   public boolean hasPinnedInstanceInitializer(DexType type) {
@@ -935,9 +951,8 @@
     return false;
   }
 
-  public Set<DexReference> getPinnedItems() {
-    assert checkIfObsolete();
-    return pinnedItems;
+  public KeepInfoCollection getKeepInfo() {
+    return keepInfo;
   }
 
   /**
@@ -987,8 +1002,6 @@
             .filter(Objects::nonNull)
             .collect(Collectors.toList()));
 
-    assert lens.assertReferencesNotModified(pinnedItems);
-
     return new AppInfoWithLiveness(
         application,
         deadProtoTypes,
@@ -1011,7 +1024,7 @@
         // TODO(sgjesse): Rewrite call sites as well? Right now they are only used by minification
         //   after second tree shaking.
         callSites,
-        pinnedItems,
+        keepInfo.rewrite(lens),
         lens.rewriteReferences(allowAccessModification),
         lens.rewriteReferenceKeys(mayHaveSideEffects),
         lens.rewriteReferenceKeys(noSideEffects),
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLivenessModifier.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLivenessModifier.java
index ae59f8b..32a8501 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLivenessModifier.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLivenessModifier.java
@@ -6,7 +6,6 @@
 
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.FieldAccessInfoCollectionImpl;
 import com.android.tools.r8.graph.FieldAccessInfoImpl;
 import com.google.common.collect.Sets;
@@ -16,7 +15,6 @@
 public class AppInfoWithLivenessModifier {
 
   private final Set<DexProgramClass> noLongerInstantiatedClasses = Sets.newConcurrentHashSet();
-  private final Set<DexReference> noLongerPinnedItems = Sets.newConcurrentHashSet();
   private final Set<DexField> noLongerWrittenFields = Sets.newConcurrentHashSet();
 
   AppInfoWithLivenessModifier() {}
@@ -29,10 +27,6 @@
     noLongerInstantiatedClasses.add(clazz);
   }
 
-  public void removePinnedClassMembers(DexProgramClass clazz) {
-    clazz.members().forEach(member -> noLongerPinnedItems.add(member.toReference()));
-  }
-
   public void removeWrittenField(DexField field) {
     noLongerWrittenFields.add(field);
   }
@@ -42,8 +36,6 @@
     noLongerInstantiatedClasses.forEach(appInfo::removeFromSingleTargetLookupCache);
     appInfo.mutateObjectAllocationInfoCollection(
         mutator -> noLongerInstantiatedClasses.forEach(mutator::markNoLongerInstantiated));
-    // Pinned items.
-    noLongerPinnedItems.forEach(appInfo::removePinnedItem);
     // Written fields.
     FieldAccessInfoCollectionImpl fieldAccessInfoCollection =
         appInfo.getMutableFieldAccessInfoCollection();
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 8d6df10..01032ab 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -92,6 +92,7 @@
 import com.android.tools.r8.shaking.DelayedRootSetActionItem.InterfaceMethodSyntheticBridgeAction;
 import com.android.tools.r8.shaking.EnqueuerWorklist.EnqueuerAction;
 import com.android.tools.r8.shaking.GraphReporter.KeepReasonWitness;
+import com.android.tools.r8.shaking.KeepInfoCollection.MutableKeepInfoCollection;
 import com.android.tools.r8.shaking.RootSetBuilder.ConsequentRootSet;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.shaking.ScopedDexMethodSet.AddMethodIfMoreVisibleResult;
@@ -305,11 +306,8 @@
    */
   private final Set<DexReference> reportedMissing = Sets.newIdentityHashSet();
 
-  /**
-   * A set of references that we are keeping due to keep rules. This may differ from the root set
-   * due to dependent keep rules.
-   */
-  private final Set<DexReference> pinnedItems = Sets.newIdentityHashSet();
+  /** Collection of keep requirements for the program. */
+  private final MutableKeepInfoCollection keepInfo = new MutableKeepInfoCollection();
 
   /**
    * A set of references that we are keeping due to keep rules, which we are allowed to publicize.
@@ -546,6 +544,10 @@
     return clazz;
   }
 
+  public boolean isPinned(DexType type) {
+    return keepInfo.isPinned(type, appInfo);
+  }
+
   private void addLiveNonProgramType(DexClass clazz) {
     assert clazz.isNotProgramClass();
     // Fast path to avoid the worklist when the class is already seen.
@@ -641,6 +643,7 @@
     if (item.isDexClass()) {
       DexProgramClass clazz = item.asDexClass().asProgramClass();
       KeepReasonWitness witness = graphReporter.reportKeepClass(precondition, rules, clazz);
+      keepInfo.pinClass(clazz);
       if (clazz.isAnnotation()) {
         workList.enqueueMarkAnnotationInstantiatedAction(clazz, witness);
       } else if (clazz.isInterface()) {
@@ -663,6 +666,7 @@
       DexEncodedField field = item.asDexEncodedField();
       DexProgramClass holder = getProgramClassOrNull(field.holder());
       if (holder != null) {
+        keepInfo.pinField(holder, field);
         workList.enqueueMarkFieldKeptAction(
             new ProgramField(holder, field),
             graphReporter.reportKeepField(precondition, rules, field));
@@ -671,6 +675,7 @@
       DexEncodedMethod encodedMethod = item.asDexEncodedMethod();
       DexProgramClass holder = getProgramClassOrNull(encodedMethod.holder());
       if (holder != null) {
+        keepInfo.pinMethod(holder, encodedMethod);
         workList.enqueueMarkMethodKeptAction(
             new ProgramMethod(holder, encodedMethod),
             graphReporter.reportKeepMethod(precondition, rules, encodedMethod));
@@ -678,7 +683,7 @@
     } else {
       throw new IllegalArgumentException(item.toString());
     }
-    addPinnedItem(item.toReference(), rules);
+    setAllowAccessModification(item.toReference(), rules);
   }
 
   private void enqueueFirstNonSerializableClassInitializer(
@@ -1314,7 +1319,7 @@
       boolean skipTracing =
           appView.withGeneratedExtensionRegistryShrinker(
               shrinker ->
-                  shrinker.isDeadProtoExtensionField(field, fieldAccessInfoCollection, pinnedItems),
+                  shrinker.isDeadProtoExtensionField(field, fieldAccessInfoCollection, keepInfo),
               false);
       if (skipTracing) {
         addDeadProtoTypeCandidate(field.getHolder());
@@ -1373,7 +1378,7 @@
       boolean skipTracing =
           appView.withGeneratedExtensionRegistryShrinker(
               shrinker ->
-                  shrinker.isDeadProtoExtensionField(field, fieldAccessInfoCollection, pinnedItems),
+                  shrinker.isDeadProtoExtensionField(field, fieldAccessInfoCollection, keepInfo),
               false);
       if (skipTracing) {
         addDeadProtoTypeCandidate(field.getHolder());
@@ -1808,12 +1813,14 @@
             holder,
             (dexType, ignored) -> {
               if (holder.isProgramClass()) {
-                DexReference holderReference = holder.toReference();
-                addPinnedItem(holderReference);
-                rootSet.shouldNotBeMinified(holderReference);
+                DexProgramClass holderClass = holder.asProgramClass();
+                keepInfo.pinClass(holderClass);
+                setAllowAccessModification(holderClass.type);
+                rootSet.shouldNotBeMinified(holder.toReference());
                 for (DexEncodedMember<?, ?> member : holder.members()) {
+                  keepInfo.pinMember(holderClass, member);
                   DexMember<?, ?> memberReference = member.toReference();
-                  addPinnedItem(memberReference);
+                  setAllowAccessModification(memberReference);
                   rootSet.shouldNotBeMinified(memberReference);
                 }
               }
@@ -2461,7 +2468,7 @@
             (type, subTypeConsumer, lambdaConsumer) ->
                 objectAllocationInfoCollection.forEachInstantiatedSubType(
                     type, subTypeConsumer, lambdaConsumer, appInfo),
-            pinnedItems::contains)
+            reference -> keepInfo.isPinned(reference, appInfo))
         .forEach(
             target ->
                 markVirtualDispatchTargetAsLive(
@@ -2528,7 +2535,8 @@
       // TODO(sgjesse): Does this have to be enqueued as a root item? Right now it is done as the
       // marking for not renaming it is in the root set.
       workList.enqueueMarkMethodKeptAction(new ProgramMethod(clazz, valuesMethod), reason);
-      addPinnedItem(valuesMethod.toReference());
+      keepInfo.pinMethod(clazz, valuesMethod);
+      setAllowAccessModification(valuesMethod.toReference());
       rootSet.shouldNotBeMinified(valuesMethod.toReference());
     }
   }
@@ -2632,16 +2640,11 @@
     return appInfoWithLiveness;
   }
 
-  public boolean isPinned(DexReference reference) {
-    return pinnedItems.contains(reference);
-  }
-
-  private boolean addPinnedItem(DexReference reference) {
+  private void setAllowAccessModification(DexReference reference) {
     allowAccessModification.put(reference, OptionalBool.unknown());
-    return pinnedItems.add(reference);
   }
 
-  private boolean addPinnedItem(DexReference reference, Set<ProguardKeepRuleBase> rules) {
+  private void setAllowAccessModification(DexReference reference, Set<ProguardKeepRuleBase> rules) {
     assert rules != null;
     assert !rules.isEmpty();
     OptionalBool allowAccessModificationOfReference =
@@ -2658,11 +2661,6 @@
       }
       allowAccessModification.put(reference, allowAccessModificationOfReference);
     }
-    return pinnedItems.add(reference);
-  }
-
-  public boolean isMissing(DexType type) {
-    return missingTypes.contains(type);
   }
 
   private static class SyntheticAdditions {
@@ -2675,7 +2673,7 @@
     Map<DexType, DexClasspathClass> syntheticClasspathClasses = new IdentityHashMap<>();
 
     // Subset of live methods that need to be pinned.
-    Set<DexMethod> pinnedMethods = Sets.newIdentityHashSet();
+    Set<ProgramMethod> pinnedMethods = Sets.newIdentityHashSet();
 
     // Subset of synthesized classes that need to be added to the main-dex file.
     Set<DexType> mainDexTypes = Sets.newIdentityHashSet();
@@ -2708,7 +2706,7 @@
 
     void addLiveAndPinnedMethod(ProgramMethod method) {
       addLiveMethod(method);
-      pinnedMethods.add(method.getDefinition().method);
+      pinnedMethods.add(method);
     }
 
     void amendApplication(Builder appBuilder) {
@@ -2727,7 +2725,11 @@
       // All synthetic additions are initial tree shaking only. No need to track keep reasons.
       KeepReasonWitness fakeReason = enqueuer.graphReporter.fakeReportShouldNotBeUsed();
 
-      pinnedMethods.forEach(enqueuer::addPinnedItem);
+      pinnedMethods.forEach(
+          method -> {
+            enqueuer.keepInfo.pinMethod(method);
+            enqueuer.setAllowAccessModification(method.getReference());
+          });
       for (Pair<DexProgramClass, ProgramMethod> clazzAndContext :
           syntheticInstantiations.values()) {
         enqueuer.workList.enqueueMarkInstantiatedAction(
@@ -2918,7 +2920,7 @@
             toImmutableSortedMap(directInvokes, PresortedComparable::slowCompare),
             toImmutableSortedMap(staticInvokes, PresortedComparable::slowCompare),
             callSites,
-            pinnedItems,
+            keepInfo,
             allowAccessModification.keySet(),
             rootSet.mayHaveSideEffects,
             rootSet.noSideEffects,
@@ -3327,7 +3329,7 @@
     assert desugaredLambdaImplementationMethods.isEmpty()
         || options.desugarState == DesugarState.ON;
     for (DexMethod method : desugaredLambdaImplementationMethods) {
-      pinnedItems.remove(method);
+      keepInfo.unpinMethod(method);
       rootSet.prune(method);
     }
     desugaredLambdaImplementationMethods.clear();
@@ -3614,7 +3616,9 @@
         workList.enqueueMarkInstantiatedAction(
             clazz, null, InstantiationReason.REFLECTION, KeepReason.reflectiveUseIn(method));
       }
-      if (addPinnedItem(encodedField.field)) {
+      if (!keepInfo.getFieldInfo(encodedField, clazz).isPinned()) {
+        keepInfo.pinField(clazz, encodedField);
+        setAllowAccessModification(encodedField.field);
         markFieldAsKept(new ProgramField(clazz, encodedField), KeepReason.reflectiveUseIn(method));
       }
     } else {
@@ -3801,14 +3805,16 @@
         // Add this interface to the set of pinned items to ensure that we do not merge the
         // interface into its unique subtype, if any.
         // TODO(b/145344105): This should be superseded by the unknown interface hierarchy.
-        addPinnedItem(clazz.type);
+        keepInfo.pinClass(clazz);
+        setAllowAccessModification(clazz.type);
         KeepReason reason = KeepReason.reflectiveUseIn(method);
         markInterfaceAsInstantiated(clazz, graphReporter.registerClass(clazz, reason));
 
         // Also pin all of its virtual methods to ensure that the devirtualizer does not perform
         // illegal rewritings of invoke-interface instructions into invoke-virtual instructions.
         for (DexEncodedMethod virtualMethod : clazz.virtualMethods()) {
-          addPinnedItem(virtualMethod.method);
+          keepInfo.pinMethod(clazz, virtualMethod);
+          setAllowAccessModification(virtualMethod.method);
           markVirtualMethodAsReachable(virtualMethod.method, true, null, reason);
         }
       }
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
new file mode 100644
index 0000000..a8cf2df
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
@@ -0,0 +1,62 @@
+// 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.shaking;
+
+/** Immutable keep requirements for a class. */
+public final class KeepClassInfo extends KeepInfo {
+
+  // Requires all aspects of a class to be kept.
+  private static final KeepClassInfo TOP = new KeepClassInfo(true);
+
+  // Requires no aspects of a class to be kept.
+  private static final KeepClassInfo BOTTOM = new KeepClassInfo(false);
+
+  public static KeepClassInfo top() {
+    return TOP;
+  }
+
+  public static KeepClassInfo bottom() {
+    return BOTTOM;
+  }
+
+  private KeepClassInfo(boolean pinned) {
+    super(pinned);
+  }
+
+  public Builder builder() {
+    return new Builder(this);
+  }
+
+  public static class Builder extends KeepInfo.Builder<Builder, KeepClassInfo> {
+
+    private Builder(KeepClassInfo original) {
+      super(original);
+    }
+
+    @Override
+    public KeepClassInfo top() {
+      return TOP;
+    }
+
+    @Override
+    public KeepClassInfo bottom() {
+      return BOTTOM;
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+
+    @Override
+    public boolean isEqualTo(KeepClassInfo other) {
+      return true;
+    }
+
+    @Override
+    public KeepClassInfo doBuild() {
+      return new KeepClassInfo(isPinned());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
new file mode 100644
index 0000000..7416d8f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
@@ -0,0 +1,62 @@
+// 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.shaking;
+
+/** Immutable keep requirements for a field. */
+public final class KeepFieldInfo extends KeepInfo {
+
+  // Requires all aspects of a field to be kept.
+  private static final KeepFieldInfo TOP = new KeepFieldInfo(true);
+
+  // Requires no aspects of a field to be kept.
+  private static final KeepFieldInfo BOTTOM = new KeepFieldInfo(false);
+
+  public static KeepFieldInfo top() {
+    return TOP;
+  }
+
+  public static KeepFieldInfo bottom() {
+    return BOTTOM;
+  }
+
+  private KeepFieldInfo(boolean pinned) {
+    super(pinned);
+  }
+
+  public Builder builder() {
+    return new Builder(this);
+  }
+
+  public static class Builder extends KeepInfo.Builder<Builder, KeepFieldInfo> {
+
+    private Builder(KeepFieldInfo original) {
+      super(original);
+    }
+
+    @Override
+    public KeepFieldInfo top() {
+      return TOP;
+    }
+
+    @Override
+    public KeepFieldInfo bottom() {
+      return BOTTOM;
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+
+    @Override
+    public boolean isEqualTo(KeepFieldInfo other) {
+      return true;
+    }
+
+    @Override
+    public KeepFieldInfo doBuild() {
+      return new KeepFieldInfo(isPinned());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
new file mode 100644
index 0000000..246eeb7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
@@ -0,0 +1,73 @@
+// 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.shaking;
+
+/** Keep information that can be associated with any item, i.e., class, method or field. */
+public abstract class KeepInfo {
+
+  private final boolean pinned;
+
+  public KeepInfo(boolean pinned) {
+    this.pinned = pinned;
+  }
+
+  public boolean isPinned() {
+    return pinned;
+  }
+
+  public abstract static class Builder<B extends Builder, K extends KeepInfo> {
+
+    public abstract B self();
+
+    public abstract K doBuild();
+
+    public abstract K top();
+
+    public abstract K bottom();
+
+    public abstract boolean isEqualTo(K other);
+
+    private K original;
+    private boolean pinned;
+
+    public Builder(K original) {
+      this.original = original;
+      pinned = original.isPinned();
+    }
+
+    public K build() {
+      if (internalIsEqualTo(original)) {
+        return original;
+      }
+      if (internalIsEqualTo(top())) {
+        return top();
+      }
+      if (internalIsEqualTo(bottom())) {
+        return bottom();
+      }
+      return doBuild();
+    }
+
+    private boolean internalIsEqualTo(K other) {
+      return isPinned() == other.isPinned() && isEqualTo(other);
+    }
+
+    public boolean isPinned() {
+      return pinned;
+    }
+
+    public B setPinned(boolean pinned) {
+      this.pinned = pinned;
+      return self();
+    }
+
+    public B pin() {
+      return setPinned(true);
+    }
+
+    public B unpin() {
+      return setPinned(false);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
new file mode 100644
index 0000000..57c1f32
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -0,0 +1,316 @@
+// 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.shaking;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.DexDefinitionSupplier;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMember;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense.NestedGraphLense;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+// Non-mutable collection of keep information pertaining to a program.
+public abstract class KeepInfoCollection {
+
+  // TODO(b/157538235): This should not be bottom.
+  private static KeepClassInfo keepInfoForNonProgramClass() {
+    return KeepClassInfo.bottom();
+  }
+
+  // TODO(b/157538235): This should not be bottom.
+  private static KeepMethodInfo keepInfoForNonProgramMethod() {
+    return KeepMethodInfo.bottom();
+  }
+
+  // TODO(b/157538235): This should not be bottom.
+  private static KeepFieldInfo keepInfoForNonProgramField() {
+    return KeepFieldInfo.bottom();
+  }
+
+  /**
+   * Base accessor for keep info on a class.
+   *
+   * <p>Access may never be granted directly on DexType as the "keep info" for any non-program type
+   * is not the same as the default keep info for a program type. By typing the interface at program
+   * item we can eliminate errors where a reference to a non-program item results in optimizations
+   * assuming aspects of it can be changed when in fact they can not.
+   */
+  public abstract KeepClassInfo getClassInfo(DexProgramClass clazz);
+
+  /**
+   * Base accessor for keep info on a method.
+   *
+   * <p>See comment on class access for why this is typed at program method.
+   */
+  public abstract KeepMethodInfo getMethodInfo(DexEncodedMethod method, DexProgramClass holder);
+
+  /**
+   * Base accessor for keep info on a field.
+   *
+   * <p>See comment on class access for why this is typed at program field.
+   */
+  public abstract KeepFieldInfo getFieldInfo(DexEncodedField field, DexProgramClass holder);
+
+  public final KeepClassInfo getClassInfo(DexType type, DexDefinitionSupplier definitions) {
+    DexProgramClass clazz = asProgramClassOrNull(definitions.definitionFor(type));
+    return clazz == null ? keepInfoForNonProgramClass() : getClassInfo(clazz);
+  }
+
+  public final KeepMethodInfo getMethodInfo(ProgramMethod method) {
+    return getMethodInfo(method.getDefinition(), method.getHolder());
+  }
+
+  public final KeepMethodInfo getMethodInfo(DexMethod method, DexDefinitionSupplier definitions) {
+    DexProgramClass holder = asProgramClassOrNull(definitions.definitionFor(method.holder));
+    if (holder == null) {
+      return keepInfoForNonProgramMethod();
+    }
+    DexEncodedMethod definition = holder.lookupMethod(method);
+    return definition == null ? KeepMethodInfo.bottom() : getMethodInfo(definition, holder);
+  }
+
+  public final KeepFieldInfo getFieldInfo(ProgramField field) {
+    return getFieldInfo(field.getDefinition(), field.getHolder());
+  }
+
+  public final KeepFieldInfo getFieldInfo(DexField field, DexDefinitionSupplier definitions) {
+    DexProgramClass holder = asProgramClassOrNull(definitions.definitionFor(field.holder));
+    if (holder == null) {
+      return keepInfoForNonProgramField();
+    }
+    DexEncodedField definition = holder.lookupField(field);
+    return definition == null ? KeepFieldInfo.bottom() : getFieldInfo(definition, holder);
+  }
+
+  public final KeepInfo getInfo(DexReference reference, DexDefinitionSupplier definitions) {
+    if (reference.isDexType()) {
+      return getClassInfo(reference.asDexType(), definitions);
+    }
+    if (reference.isDexMethod()) {
+      return getMethodInfo(reference.asDexMethod(), definitions);
+    }
+    if (reference.isDexField()) {
+      return getFieldInfo(reference.asDexField(), definitions);
+    }
+    throw new Unreachable();
+  }
+
+  public final boolean isPinned(DexReference reference, DexDefinitionSupplier definitions) {
+    return getInfo(reference, definitions).isPinned();
+  }
+
+  public final boolean isPinned(DexType type, DexDefinitionSupplier definitions) {
+    return getClassInfo(type, definitions).isPinned();
+  }
+
+  public final boolean isPinned(DexMethod method, DexDefinitionSupplier definitions) {
+    return getMethodInfo(method, definitions).isPinned();
+  }
+
+  public final boolean isPinned(DexField field, DexDefinitionSupplier definitions) {
+    return getFieldInfo(field, definitions).isPinned();
+  }
+
+  public final boolean verifyNoneArePinned(Collection<DexType> types, AppInfo appInfo) {
+    for (DexType type : types) {
+      DexProgramClass clazz =
+          asProgramClassOrNull(appInfo.definitionForWithoutExistenceAssert(type));
+      assert clazz == null || !getClassInfo(clazz).isPinned();
+    }
+    return true;
+  }
+
+  // TODO(b/156715504): We should try to avoid the need for iterating pinned items.
+  @Deprecated
+  public abstract void forEachPinnedType(Consumer<DexType> consumer);
+
+  // TODO(b/156715504): We should try to avoid the need for iterating pinned items.
+  @Deprecated
+  public abstract void forEachPinnedMethod(Consumer<DexMethod> consumer);
+
+  // TODO(b/156715504): We should try to avoid the need for iterating pinned items.
+  @Deprecated
+  public abstract void forEachPinnedField(Consumer<DexField> consumer);
+
+  public abstract KeepInfoCollection rewrite(NestedGraphLense lens);
+
+  public abstract KeepInfoCollection mutate(Consumer<MutableKeepInfoCollection> mutator);
+
+  // Mutation interface for building up the keep info.
+  public static class MutableKeepInfoCollection extends KeepInfoCollection {
+
+    // These are typed at signatures but the interface should make sure never to allow access
+    // directly with a signature. See the comment in KeepInfoCollection.
+    private final Map<DexType, KeepClassInfo> keepClassInfo;
+    private final Map<DexMethod, KeepMethodInfo> keepMethodInfo;
+    private final Map<DexField, KeepFieldInfo> keepFieldInfo;
+
+    MutableKeepInfoCollection() {
+      this(new IdentityHashMap<>(), new IdentityHashMap<>(), new IdentityHashMap<>());
+    }
+
+    private MutableKeepInfoCollection(
+        Map<DexType, KeepClassInfo> keepClassInfo,
+        Map<DexMethod, KeepMethodInfo> keepMethodInfo,
+        Map<DexField, KeepFieldInfo> keepFieldInfo) {
+      this.keepClassInfo = keepClassInfo;
+      this.keepMethodInfo = keepMethodInfo;
+      this.keepFieldInfo = keepFieldInfo;
+    }
+
+    @Override
+    public KeepInfoCollection rewrite(NestedGraphLense lens) {
+      Map<DexType, KeepClassInfo> newClassInfo = new IdentityHashMap<>(keepClassInfo.size());
+      keepClassInfo.forEach(
+          (type, info) -> {
+            DexType newType = lens.lookupType(type);
+            assert !info.isPinned() || type == newType;
+            newClassInfo.put(newType, info);
+          });
+      Map<DexMethod, KeepMethodInfo> newMethodInfo = new IdentityHashMap<>(keepMethodInfo.size());
+      keepMethodInfo.forEach(
+          (method, info) -> {
+            DexMethod newMethod = lens.getRenamedMethodSignature(method);
+            assert !info.isPinned() || method == newMethod;
+            newMethodInfo.put(newMethod, info);
+          });
+      Map<DexField, KeepFieldInfo> newFieldInfo = new IdentityHashMap<>(keepFieldInfo.size());
+      keepFieldInfo.forEach(
+          (field, info) -> {
+            DexField newField = lens.getRenamedFieldSignature(field);
+            assert !info.isPinned() || field == newField;
+            newFieldInfo.put(newField, info);
+          });
+      return new MutableKeepInfoCollection(newClassInfo, newMethodInfo, newFieldInfo);
+    }
+
+    @Override
+    public KeepClassInfo getClassInfo(DexProgramClass clazz) {
+      return keepClassInfo.getOrDefault(clazz.type, KeepClassInfo.bottom());
+    }
+
+    @Override
+    public KeepMethodInfo getMethodInfo(DexEncodedMethod method, DexProgramClass holder) {
+      assert method.holder() == holder.type;
+      return keepMethodInfo.getOrDefault(method.method, KeepMethodInfo.bottom());
+    }
+
+    @Override
+    public KeepFieldInfo getFieldInfo(DexEncodedField field, DexProgramClass holder) {
+      assert field.holder() == holder.type;
+      return keepFieldInfo.getOrDefault(field.field, KeepFieldInfo.bottom());
+    }
+
+    public void pinClass(DexProgramClass clazz) {
+      KeepClassInfo info = getClassInfo(clazz);
+      if (!info.isPinned()) {
+        keepClassInfo.put(clazz.type, info.builder().pin().build());
+      }
+    }
+
+    public void pinMember(DexProgramClass holder, DexEncodedMember<?, ?> member) {
+      if (member.isDexEncodedMethod()) {
+        pinMethod(holder, member.asDexEncodedMethod());
+      } else {
+        assert member.isDexEncodedField();
+        pinField(holder, member.asDexEncodedField());
+      }
+    }
+
+    public void pinMethod(ProgramMethod programMethod) {
+      pinMethod(programMethod.getHolder(), programMethod.getDefinition());
+    }
+
+    public void pinMethod(DexProgramClass holder, DexEncodedMethod method) {
+      KeepMethodInfo info = getMethodInfo(method, holder);
+      if (!info.isPinned()) {
+        keepMethodInfo.put(method.method, info.builder().pin().build());
+      }
+    }
+
+    public void unpinMethod(ProgramMethod method) {
+      assert !getClassInfo(method.getHolder()).isPinned();
+      unpinMethod(method.getReference());
+    }
+
+    // TODO(b/156715504): We should never need to unpin items. Rather avoid pinning to begin with.
+    @Deprecated
+    public void unpinMethod(DexMethod method) {
+      KeepMethodInfo info = keepMethodInfo.get(method);
+      if (info != null && info.isPinned()) {
+        keepMethodInfo.put(method, info.builder().unpin().build());
+      }
+    }
+
+    public void pinField(ProgramField programField) {
+      pinField(programField.getHolder(), programField.getDefinition());
+    }
+
+    public void pinField(DexProgramClass holder, DexEncodedField field) {
+      KeepFieldInfo info = getFieldInfo(field, holder);
+      if (!info.isPinned()) {
+        keepFieldInfo.put(field.field, info.builder().pin().build());
+      }
+    }
+
+    public void unpinField(DexProgramClass holder, DexEncodedField field) {
+      assert holder.type == field.holder();
+      assert !getClassInfo(holder).isPinned();
+      KeepFieldInfo info = this.keepFieldInfo.get(field.toReference());
+      if (info != null && info.isPinned()) {
+        keepFieldInfo.put(field.toReference(), info.builder().unpin().build());
+      }
+    }
+
+    @Override
+    public KeepInfoCollection mutate(Consumer<MutableKeepInfoCollection> mutator) {
+      mutator.accept(this);
+      return this;
+    }
+
+    @Override
+    public void forEachPinnedType(Consumer<DexType> consumer) {
+      keepClassInfo.forEach(
+          (type, info) -> {
+            if (info.isPinned()) {
+              consumer.accept(type);
+            }
+          });
+    }
+
+    @Override
+    public void forEachPinnedMethod(Consumer<DexMethod> consumer) {
+      keepMethodInfo.forEach(
+          (method, info) -> {
+            if (info.isPinned()) {
+              consumer.accept(method);
+            }
+          });
+    }
+
+    @Override
+    public void forEachPinnedField(Consumer<DexField> consumer) {
+      keepFieldInfo.forEach(
+          (field, info) -> {
+            if (info.isPinned()) {
+              consumer.accept(field);
+            }
+          });
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
new file mode 100644
index 0000000..f3bb073
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
@@ -0,0 +1,62 @@
+// 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.shaking;
+
+/** Immutable keep requirements for a method. */
+public final class KeepMethodInfo extends KeepInfo {
+
+  // Requires all aspects of a method to be kept.
+  private static final KeepMethodInfo TOP = new KeepMethodInfo(true);
+
+  // Requires no aspects of a method to be kept.
+  private static final KeepMethodInfo BOTTOM = new KeepMethodInfo(false);
+
+  public static KeepMethodInfo top() {
+    return TOP;
+  }
+
+  public static KeepMethodInfo bottom() {
+    return BOTTOM;
+  }
+
+  private KeepMethodInfo(boolean pinned) {
+    super(pinned);
+  }
+
+  public Builder builder() {
+    return new Builder(this);
+  }
+
+  public static class Builder extends KeepInfo.Builder<Builder, KeepMethodInfo> {
+
+    private Builder(KeepMethodInfo original) {
+      super(original);
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+
+    @Override
+    public KeepMethodInfo top() {
+      return TOP;
+    }
+
+    @Override
+    public KeepMethodInfo bottom() {
+      return BOTTOM;
+    }
+
+    @Override
+    public boolean isEqualTo(KeepMethodInfo other) {
+      return true;
+    }
+
+    @Override
+    public KeepMethodInfo doBuild() {
+      return new KeepMethodInfo(isPinned());
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java b/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
index 0979ef9..c6a1725 100644
--- a/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
+++ b/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
@@ -50,8 +49,10 @@
         getClassesWithLibraryMethodOverrides(appView);
 
     // Remove all types that are pinned from the initial set of non-escaping classes.
-    DexReference.filterDexType(appView.appInfo().pinnedItems.stream())
-        .forEach(initialNonEscapingClassesWithLibraryMethodOverrides::remove);
+    appView
+        .appInfo()
+        .getKeepInfo()
+        .forEachPinnedType(initialNonEscapingClassesWithLibraryMethodOverrides::remove);
 
     return initialNonEscapingClassesWithLibraryMethodOverrides;
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index ffb940a..859e313 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -689,53 +689,59 @@
   // TODO(b/67934426): Test this code.
   public static void writeSeeds(
       AppInfoWithLiveness appInfo, PrintStream out, Predicate<DexType> include) {
-    for (DexReference seed : appInfo.getPinnedItems()) {
-      if (seed.isDexType()) {
-        if (include.test(seed.asDexType())) {
-          out.println(seed.toSourceString());
-        }
-      } else if (seed.isDexField()) {
-        DexField field = seed.asDexField();
-        if (include.test(field.holder)) {
-          out.println(
-              field.holder.toSourceString()
-                  + ": "
-                  + field.type.toSourceString()
-                  + " "
-                  + field.name.toSourceString());
-        }
-      } else {
-        assert seed.isDexMethod();
-        DexMethod method = seed.asDexMethod();
-        if (!include.test(method.holder)) {
-          continue;
-        }
-        out.print(method.holder.toSourceString() + ": ");
-        DexEncodedMethod encodedMethod = appInfo.definitionFor(method);
-        if (encodedMethod.accessFlags.isConstructor()) {
-          if (encodedMethod.accessFlags.isStatic()) {
-            out.print(Constants.CLASS_INITIALIZER_NAME);
-          } else {
-            String holderName = method.holder.toSourceString();
-            String constrName = holderName.substring(holderName.lastIndexOf('.') + 1);
-            out.print(constrName);
-          }
-        } else {
-          out.print(
-              method.proto.returnType.toSourceString() + " " + method.name.toSourceString());
-        }
-        boolean first = true;
-        out.print("(");
-        for (DexType param : method.proto.parameters.values) {
-          if (!first) {
-            out.print(",");
-          }
-          first = false;
-          out.print(param.toSourceString());
-        }
-        out.println(")");
-      }
-    }
+    appInfo
+        .getKeepInfo()
+        .forEachPinnedType(
+            type -> {
+              if (include.test(type)) {
+                out.println(type.toSourceString());
+              }
+            });
+    appInfo
+        .getKeepInfo()
+        .forEachPinnedField(
+            field -> {
+              if (include.test(field.holder)) {
+                out.println(
+                    field.holder.toSourceString()
+                        + ": "
+                        + field.type.toSourceString()
+                        + " "
+                        + field.name.toSourceString());
+              }
+            });
+    appInfo
+        .getKeepInfo()
+        .forEachPinnedMethod(
+            method -> {
+              if (!include.test(method.holder)) {
+                return;
+              }
+              out.print(method.holder.toSourceString() + ": ");
+              DexEncodedMethod encodedMethod = appInfo.definitionFor(method);
+              if (encodedMethod.accessFlags.isConstructor()) {
+                if (encodedMethod.accessFlags.isStatic()) {
+                  out.print(Constants.CLASS_INITIALIZER_NAME);
+                } else {
+                  String holderName = method.holder.toSourceString();
+                  String constrName = holderName.substring(holderName.lastIndexOf('.') + 1);
+                  out.print(constrName);
+                }
+              } else {
+                out.print(
+                    method.proto.returnType.toSourceString() + " " + method.name.toSourceString());
+              }
+              boolean first = true;
+              out.print("(");
+              for (DexType param : method.proto.parameters.values) {
+                if (!first) {
+                  out.print(",");
+                }
+                first = false;
+                out.print(param.toSourceString());
+              }
+              out.println(")");
+            });
     out.close();
   }
 
@@ -1643,14 +1649,11 @@
     }
 
     public boolean verifyKeptItemsAreKept(DexApplication application, AppInfo appInfo) {
-      Set<DexReference> pinnedItems =
-          appInfo.hasLiveness() ? appInfo.withLiveness().pinnedItems : null;
-
       // Create a mapping from each required type to the set of required members on that type.
       Map<DexType, Set<DexReference>> requiredReferencesPerType = new IdentityHashMap<>();
       for (DexReference reference : noShrinking.keySet()) {
         // Check that `pinnedItems` is a super set of the root set.
-        assert pinnedItems == null || pinnedItems.contains(reference)
+        assert !appInfo.hasLiveness() || appInfo.withLiveness().isPinned(reference)
             : "Expected reference `" + reference.toSourceString() + "` to be pinned";
         if (reference.isDexType()) {
           DexType type = reference.asDexType();
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 da9038d..3dd0f4f 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -273,7 +273,12 @@
     // For all pinned fields, also pin the type of the field (because changing the type of the field
     // implicitly changes the signature of the field). Similarly, for all pinned methods, also pin
     // the return type and the parameter types of the method.
-    extractPinnedItems(appInfo.pinnedItems, AbortReason.PINNED_SOURCE);
+    // TODO(b/156715504): Compute referenced-by-pinned in the keep info objects.
+    List<DexReference> pinnedItems = new ArrayList<>();
+    appInfo.getKeepInfo().forEachPinnedType(pinnedItems::add);
+    appInfo.getKeepInfo().forEachPinnedMethod(pinnedItems::add);
+    appInfo.getKeepInfo().forEachPinnedField(pinnedItems::add);
+    extractPinnedItems(pinnedItems, AbortReason.PINNED_SOURCE);
 
     // TODO(christofferqa): Remove the invariant that the graph lense should not modify any
     // methods from the sets alwaysInline and noSideEffects (see use of assertNotModified).
@@ -680,7 +685,7 @@
     // that `invoke-super A.method` instructions, which are in one of the methods from C, needs to
     // be rewritten to `invoke-direct C.method$B`. This is valid even though A.method() is actually
     // pinned, because this rewriting does not affect A.method() in any way.
-    assert graphLense.assertReferencesNotModified(appInfo.pinnedItems);
+    assert graphLense.assertPinnedNotModified(appInfo.getKeepInfo());
 
     for (DexProgramClass clazz : appInfo.classes()) {
       for (DexEncodedMethod encodedMethod : clazz.methods()) {
diff --git a/src/test/java/com/android/tools/r8/shaking/FieldTypeTest.java b/src/test/java/com/android/tools/r8/shaking/FieldTypeTest.java
index 105bf6b..512a9ff 100644
--- a/src/test/java/com/android/tools/r8/shaking/FieldTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/FieldTypeTest.java
@@ -103,8 +103,7 @@
         */
         "getstatic " + client.name + "/" + obj2.name + " " + itf2.getDescriptor(),
         "invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
-        "return"
-    );
+        "return");
 
     final String mainClassName = mainClass.name;
     String proguardConfig = StringUtils.lines(