Reapply "[KeepAnno] Partial implementation of native interpretation"

This reverts commit 916906dad3493c6b6ee3fdf557a99f98866f6c10.

Change-Id: I010cf6065378efc0ec6ec6e2712038b85be4f627
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemReference.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemReference.java
index 5a3c7d5..34a97b6 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemReference.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepClassItemReference.java
@@ -5,6 +5,8 @@
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.function.Consumer;
+import java.util.function.Function;
 
 public abstract class KeepClassItemReference extends KeepItemReference {
 
@@ -28,6 +30,19 @@
     return this;
   }
 
+  public final <T> T applyClassItemReference(
+      Function<KeepBindingReference, T> onBinding, Function<KeepClassItemPattern, T> onPattern) {
+    if (isBindingReference()) {
+      return onBinding.apply(asBindingReference());
+    }
+    return onPattern.apply(asClassItemPattern());
+  }
+
+  public final void matchClassItemReference(
+      Consumer<KeepBindingReference> onBinding, Consumer<KeepClassItemPattern> onPattern) {
+    applyClassItemReference(AstUtils.toVoidFunction(onBinding), AstUtils.toVoidFunction(onPattern));
+  }
+
   public abstract Collection<KeepBindingReference> getBindingReferences();
 
   private static class ClassBinding extends KeepClassItemReference {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraint.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraint.java
index 24ec53d..5368d5e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraint.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraint.java
@@ -25,6 +25,8 @@
     return System.identityHashCode(this);
   }
 
+  public abstract void accept(KeepConstraintVisitor visitor);
+
   public abstract String getEnumValue();
 
   public KeepAnnotationPattern asAnnotationPattern() {
@@ -77,6 +79,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onLookup(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.SHRINKING);
     }
@@ -98,6 +105,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onName(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.OBFUSCATING);
     }
@@ -119,6 +131,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onVisibilityRelax(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       // The compiler currently satisfies that access is never restricted.
     }
@@ -140,6 +157,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onVisibilityRestrict(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       // We don't have directional rules so this prohibits any modification.
       builder.add(KeepOption.ACCESS_MODIFICATION);
@@ -162,6 +184,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onNeverInline(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.OPTIMIZING);
     }
@@ -188,6 +215,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onClassInstantiate(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.OPTIMIZING);
     }
@@ -214,6 +246,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onClassOpenHierarchy(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.OPTIMIZING);
     }
@@ -240,6 +277,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onMethodInvoke(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.OPTIMIZING);
     }
@@ -266,6 +308,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onMethodReplace(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.OPTIMIZING);
     }
@@ -292,6 +339,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onFieldGet(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.OPTIMIZING);
     }
@@ -318,6 +370,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onFieldSet(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.OPTIMIZING);
     }
@@ -344,6 +401,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onFieldReplace(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       builder.add(KeepOption.OPTIMIZING);
     }
@@ -403,6 +465,11 @@
     }
 
     @Override
+    public void accept(KeepConstraintVisitor visitor) {
+      visitor.onAnnotation(this);
+    }
+
+    @Override
     public void convertToDisallowKeepOptions(KeepOptions.Builder builder) {
       // The annotation constraint only implies that annotations should remain, no restrictions
       // are on the item otherwise. Also, we can't restrict the rule to just the annotations being
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraintVisitor.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraintVisitor.java
new file mode 100644
index 0000000..f7db601
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraintVisitor.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.keepanno.ast;
+
+import com.android.tools.r8.keepanno.ast.KeepConstraint.Annotation;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.ClassInstantiate;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.ClassOpenHierarchy;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.FieldGet;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.FieldReplace;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.FieldSet;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.Lookup;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.MethodInvoke;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.MethodReplace;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.Name;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.NeverInline;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.VisibilityRelax;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.VisibilityRestrict;
+
+public abstract class KeepConstraintVisitor {
+
+  public abstract void onLookup(Lookup constraint);
+
+  public abstract void onName(Name constraint);
+
+  public abstract void onVisibilityRelax(VisibilityRelax constraint);
+
+  public abstract void onVisibilityRestrict(VisibilityRestrict constraint);
+
+  public abstract void onNeverInline(NeverInline constraint);
+
+  public abstract void onClassInstantiate(ClassInstantiate constraint);
+
+  public abstract void onClassOpenHierarchy(ClassOpenHierarchy constraint);
+
+  public abstract void onMethodInvoke(MethodInvoke constraint);
+
+  public abstract void onMethodReplace(MethodReplace constraint);
+
+  public abstract void onFieldGet(FieldGet constraint);
+
+  public abstract void onFieldSet(FieldSet constraint);
+
+  public abstract void onFieldReplace(FieldReplace constraint);
+
+  public abstract void onAnnotation(Annotation constraint);
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraints.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraints.java
index 746d22a..12f10b0 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraints.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConstraints.java
@@ -33,6 +33,10 @@
     return new Builder();
   }
 
+  public void forEachAccept(KeepConstraintVisitor visitor) {
+    getConstraints().forEach(c -> c.accept(visitor));
+  }
+
   public static class Builder {
 
     private boolean defaultAdditions = false;
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index ca3f045..7597634 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -507,7 +507,7 @@
     }
 
     @Deprecated
-    public Builder setEnableExperimentalVersionedKeepEdgeAnnotations(boolean enable) {
+    public Builder setEnableExperimentalExtractedKeepAnnotations(boolean enable) {
       this.enableExperimentalVersionedKeepEdgeAnnotations = enable;
       return self();
     }
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 4809783..f597df0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -303,6 +303,11 @@
     return DescriptorUtils.isPrimitiveType(descriptor.getFirstByteAsChar());
   }
 
+  public char asPrimitiveTypeDescriptorChar() {
+    assert isPrimitiveType();
+    return descriptor.getFirstByteAsChar();
+  }
+
   public boolean isVoidType() {
     return descriptor.getFirstByteAsChar() == 'V';
   }
@@ -496,12 +501,25 @@
     return dexItemFactory.createType(descriptor.toArrayDescriptor(dimensions, dexItemFactory));
   }
 
+  public int getArrayTypeDimensions() {
+    for (int i = 0; i < descriptor.content.length; i++) {
+      if (descriptor.content[i] != '[') {
+        return i;
+      }
+    }
+    return 0;
+  }
+
   public DexType toArrayElementType(DexItemFactory dexItemFactory) {
-    assert isArrayType();
+    return toArrayElementAfterDimension(1, dexItemFactory);
+  }
+
+  public DexType toArrayElementAfterDimension(int dimension, DexItemFactory dexItemFactory) {
+    assert getArrayTypeDimensions() >= dimension;
     DexString newDesc =
         dexItemFactory.createString(
-            descriptor.size - 1,
-            Arrays.copyOfRange(descriptor.content, 1, descriptor.content.length));
+            descriptor.size - dimension,
+            Arrays.copyOfRange(descriptor.content, dimension, descriptor.content.length));
     return dexItemFactory.createType(newDesc);
   }
 
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 9ea2577..1fd03eb 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -25,7 +25,6 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.code.CfOrDexInstruction;
 import com.android.tools.r8.errors.InterfaceDesugarMissingTypeDiagnostic;
-import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
 import com.android.tools.r8.features.IsolatedFeatureSplitsChecker;
@@ -153,6 +152,8 @@
 import com.android.tools.r8.shaking.RootSetUtils.RootSetBase;
 import com.android.tools.r8.shaking.RootSetUtils.RootSetBuilder;
 import com.android.tools.r8.shaking.ScopedDexMethodSet.AddMethodIfMoreVisibleResult;
+import com.android.tools.r8.shaking.rules.ApplicableRulesEvaluator;
+import com.android.tools.r8.shaking.rules.KeepAnnotationMatcher;
 import com.android.tools.r8.synthesis.SyntheticItems.SynthesizingContextOracle;
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.Box;
@@ -290,6 +291,7 @@
   private final Set<DexMember<?, ?>> identifierNameStrings = Sets.newIdentityHashSet();
 
   private List<KeepDeclaration> keepDeclarations = Collections.emptyList();
+  private ApplicableRulesEvaluator applicableRules = ApplicableRulesEvaluator.empty();
 
   /**
    * Tracks the dependency between a method and the super-method it calls, if any. Used to make
@@ -3797,17 +3799,18 @@
     includeMinimumKeepInfo(rootSet);
     timing.end();
 
+    assert applicableRules == ApplicableRulesEvaluator.empty();
     if (mode.isInitialTreeShaking()) {
-      // TODO(b/323816623): Start native interpretation here...
-      if (!keepDeclarations.isEmpty()) {
-        throw new Unimplemented("Native support for keep annotaitons pending");
-      }
+      applicableRules =
+          KeepAnnotationMatcher.computeInitialRules(
+              appInfo, keepDeclarations, options.getThreadingModule(), executorService);
       // Amend library methods with covariant return types.
       timing.begin("Model library");
       modelLibraryMethodsWithCovariantReturnTypes(appView);
       timing.end();
     } else if (appView.getKeepInfo() != null) {
       timing.begin("Retain keep info");
+      applicableRules = appView.getKeepInfo().getApplicableRules();
       EnqueuerEvent preconditionEvent = UnconditionalKeepInfoEvent.get();
       appView
           .getKeepInfo()
@@ -3820,6 +3823,7 @@
               this::applyMinimumKeepInfoWhenLiveOrTargeted);
       timing.end();
     }
+    timing.time("Unconditional rules", () -> applicableRules.evaluateUnconditionalRules(this));
     timing.begin("Enqueue all");
     enqueueAllIfNotShrinking();
     timing.end();
@@ -3876,6 +3880,14 @@
             this::recordDependentMinimumKeepInfo);
   }
 
+  public void includeMinimumKeepInfo(MinimumKeepInfoCollection minimumKeepInfo) {
+    minimumKeepInfo.forEach(
+        appView,
+        (i, j) -> recordDependentMinimumKeepInfo(EnqueuerEvent.unconditional(), i, j),
+        (i, j) -> recordDependentMinimumKeepInfo(EnqueuerEvent.unconditional(), i, j),
+        (i, j) -> recordDependentMinimumKeepInfo(EnqueuerEvent.unconditional(), i, j));
+  }
+
   private void applyMinimumKeepInfo(DexProgramClass clazz) {
     EnqueuerEvent preconditionEvent = UnconditionalKeepInfoEvent.get();
     KeepClassInfo.Joiner minimumKeepInfoForClass =
@@ -4442,6 +4454,8 @@
               : ImmutableSet.of(syntheticClass.getType());
         };
     amendKeepInfoWithCompanionMethods();
+    keepInfo.setMaterializedRules(applicableRules.getMaterializedRules());
+
     timing.begin("Rewrite with deferred results");
     deferredTracing.rewriteApplication(executorService);
     timing.end();
@@ -4629,6 +4643,8 @@
         // Continue fix-point processing if -if rules are enabled by items that newly became live.
         long numberOfLiveItemsAfterProcessing = getNumberOfLiveItems();
         if (numberOfLiveItemsAfterProcessing > numberOfLiveItems) {
+          timing.time("Conditional rules", () -> applicableRules.evaluateConditionalRules(this));
+
           // Build the mapping of active if rules. We use a single collection of if-rules to allow
           // removing if rules that have a constant sequent keep rule when they materialize.
           if (activeIfRules == null) {
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index 8100e4e..cd76ad3 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -29,6 +29,8 @@
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
 import com.android.tools.r8.shaking.KeepFieldInfo.Joiner;
+import com.android.tools.r8.shaking.rules.ApplicableRulesEvaluator;
+import com.android.tools.r8.shaking.rules.MaterializedRules;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.MapUtils;
@@ -264,6 +266,8 @@
 
   public abstract void writeToDirectory(Path directory) throws IOException;
 
+  public abstract ApplicableRulesEvaluator getApplicableRules();
+
   // Mutation interface for building up the keep info.
   public static class MutableKeepInfoCollection extends KeepInfoCollection {
 
@@ -278,6 +282,9 @@
     private final Map<DexField, KeepFieldInfo.Joiner> fieldRuleInstances;
     private final Map<DexMethod, KeepMethodInfo.Joiner> methodRuleInstances;
 
+    // Collection of materialized rules.
+    private MaterializedRules materializedRules;
+
     MutableKeepInfoCollection() {
       this(
           new IdentityHashMap<>(),
@@ -285,7 +292,8 @@
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
-          new IdentityHashMap<>());
+          new IdentityHashMap<>(),
+          MaterializedRules.empty());
     }
 
     private MutableKeepInfoCollection(
@@ -294,13 +302,26 @@
         Map<DexField, KeepFieldInfo> keepFieldInfo,
         Map<DexType, KeepClassInfo.Joiner> classRuleInstances,
         Map<DexField, KeepFieldInfo.Joiner> fieldRuleInstances,
-        Map<DexMethod, KeepMethodInfo.Joiner> methodRuleInstances) {
+        Map<DexMethod, KeepMethodInfo.Joiner> methodRuleInstances,
+        MaterializedRules materializedRules) {
       this.keepClassInfo = keepClassInfo;
       this.keepMethodInfo = keepMethodInfo;
       this.keepFieldInfo = keepFieldInfo;
       this.classRuleInstances = classRuleInstances;
       this.fieldRuleInstances = fieldRuleInstances;
       this.methodRuleInstances = methodRuleInstances;
+      this.materializedRules = materializedRules;
+    }
+
+    public void setMaterializedRules(MaterializedRules materializedRules) {
+      assert this.materializedRules == MaterializedRules.empty();
+      assert materializedRules != null;
+      this.materializedRules = materializedRules;
+    }
+
+    @Override
+    public ApplicableRulesEvaluator getApplicableRules() {
+      return materializedRules.toApplicableRules();
     }
 
     public void removeKeepInfoForMergedClasses(PrunedItems prunedItems) {
@@ -360,12 +381,12 @@
               rewriteRuleInstances(
                   methodRuleInstances,
                   lens::getRenamedMethodSignature,
-                  KeepMethodInfo::newEmptyJoiner));
+                  KeepMethodInfo::newEmptyJoiner),
+              materializedRules.rewriteWithLens(lens));
       timing.end();
       return result;
     }
 
-    @SuppressWarnings("ReferenceEquality")
     private Map<DexType, KeepClassInfo> rewriteClassInfo(
         NonIdentityGraphLens lens, InternalOptions options, Timing timing) {
       timing.begin("Rewrite class info");
@@ -373,11 +394,11 @@
       keepClassInfo.forEach(
           (type, info) -> {
             DexType newType = lens.lookupType(type);
-            if (newType == options.dexItemFactory().intType) {
+            if (options.dexItemFactory().intType.isIdenticalTo(newType)) {
               assert !info.isPinned(options);
               return;
             }
-            assert newType == type
+            assert type.isIdenticalTo(newType)
                 || !info.isPinned(options)
                 || info.isMinificationAllowed(options)
                 || info.isRepackagingAllowed(options);
@@ -388,7 +409,6 @@
       return newClassInfo;
     }
 
-    @SuppressWarnings("ReferenceEquality")
     private Map<DexField, KeepFieldInfo> rewriteFieldInfo(
         NonIdentityGraphLens lens, InternalOptions options, Timing timing) {
       timing.begin("Rewrite field info");
@@ -396,7 +416,7 @@
       keepFieldInfo.forEach(
           (field, info) -> {
             DexField newField = lens.getRenamedFieldSignature(field);
-            assert newField.name == field.name
+            assert newField.name.isIdenticalTo(field.name)
                 || !info.isPinned(options)
                 || info.isMinificationAllowed(options);
             KeepFieldInfo previous = newFieldInfo.put(newField, info);
@@ -406,7 +426,7 @@
       return newFieldInfo;
     }
 
-    @SuppressWarnings({"ReferenceEquality", "UnusedVariable"})
+    @SuppressWarnings("UnusedVariable")
     private Map<DexMethod, KeepMethodInfo> rewriteMethodInfo(
         NonIdentityGraphLens lens, InternalOptions options, Timing timing) {
       timing.begin("Rewrite method info");
@@ -416,7 +436,7 @@
             DexMethod newMethod = lens.getRenamedMethodSignature(method);
             assert !info.isPinned(options)
                 || info.isMinificationAllowed(options)
-                || newMethod.name == method.name;
+                || newMethod.name.isIdenticalTo(method.name);
             assert !info.isPinned(options) || newMethod.getArity() == method.getArity();
             assert !info.isPinned(options)
                 || Streams.zip(
@@ -425,7 +445,7 @@
                         Object::equals)
                     .allMatch(x -> x);
             assert !info.isPinned(options)
-                || newMethod.getReturnType() == lens.lookupType(method.getReturnType());
+                || newMethod.getReturnType().isIdenticalTo(lens.lookupType(method.getReturnType()));
             KeepMethodInfo previous = newMethodInfo.put(newMethod, info);
             // TODO(b/169927809): Avoid collisions.
             // assert previous == null;
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluator.java b/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluator.java
new file mode 100644
index 0000000..ea65365
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluator.java
@@ -0,0 +1,75 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.MinimumKeepInfoCollection;
+import java.util.ArrayList;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.function.Consumer;
+
+public abstract class ApplicableRulesEvaluator {
+
+  public static ApplicableRulesEvaluator empty() {
+    return EmptyEvaluator.INSTANCE;
+  }
+
+  public abstract void evaluateUnconditionalRules(Enqueuer enqueuer);
+
+  public abstract void evaluateConditionalRules(Enqueuer enqueuer);
+
+  public abstract MaterializedRules getMaterializedRules();
+
+  public static Builder initialRulesBuilder() {
+    return new Builder();
+  }
+
+  private static class EmptyEvaluator extends ApplicableRulesEvaluator {
+
+    private static EmptyEvaluator INSTANCE = new EmptyEvaluator();
+
+    private EmptyEvaluator() {}
+
+    @Override
+    public void evaluateUnconditionalRules(Enqueuer enqueuer) {
+      // Nothing to do.
+    }
+
+    @Override
+    public void evaluateConditionalRules(Enqueuer enqueuer) {
+      // Nothing to do.
+    }
+
+    @Override
+    public MaterializedRules getMaterializedRules() {
+      return MaterializedRules.empty();
+    }
+  }
+
+  public static class Builder {
+
+    private MinimumKeepInfoCollection rootConsequences =
+        MinimumKeepInfoCollection.createConcurrent();
+    private ConcurrentLinkedDeque<PendingInitialConditionalRule> rules =
+        new ConcurrentLinkedDeque<>();
+
+    private Builder() {}
+
+    public void addRootRule(Consumer<MinimumKeepInfoCollection> fn) {
+      fn.accept(rootConsequences);
+    }
+
+    public void addConditionalRule(PendingInitialConditionalRule rule) {
+      rules.add(rule);
+    }
+
+    public ApplicableRulesEvaluator build() {
+      if (rootConsequences.isEmpty() && rules.isEmpty()) {
+        return ApplicableRulesEvaluator.empty();
+      }
+      return new ApplicableRulesEvaluatorImpl<>(rootConsequences, new ArrayList<>(rules));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluatorImpl.java b/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluatorImpl.java
new file mode 100644
index 0000000..c182c24
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/ApplicableRulesEvaluatorImpl.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.MinimumKeepInfoCollection;
+import com.android.tools.r8.utils.ListUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class ApplicableRulesEvaluatorImpl<T> extends ApplicableRulesEvaluator {
+
+  private final MinimumKeepInfoCollection rootConsequences;
+
+  // TODO(b/323816623): Revaluate these numbers. They are set low/tight now to hit in tests.
+  private static final int reallocMinThreshold = 1;
+  private static final int reallocRatioThreshold = 10;
+  private int prunedCount = 0;
+  private List<PendingConditionalRuleBase<T>> pendingConditionalRules;
+
+  private final List<MaterializedConditionalRule> materializedRules = new ArrayList<>();
+
+  ApplicableRulesEvaluatorImpl(
+      MinimumKeepInfoCollection rootConsequences,
+      List<PendingConditionalRuleBase<T>> conditionalRules) {
+    assert !rootConsequences.isEmpty() || !conditionalRules.isEmpty();
+    this.rootConsequences = rootConsequences;
+    this.pendingConditionalRules = conditionalRules;
+  }
+
+  @Override
+  public void evaluateUnconditionalRules(Enqueuer enqueuer) {
+    assert materializedRules.isEmpty();
+    if (!rootConsequences.isEmpty()) {
+      enqueuer.includeMinimumKeepInfo(rootConsequences);
+    }
+  }
+
+  @Override
+  public void evaluateConditionalRules(Enqueuer enqueuer) {
+    if (pendingConditionalRules.isEmpty()) {
+      return;
+    }
+    // TODO(b/323816623): If we tracked newly live, we could speed up finding rules.
+    // TODO(b/323816623): Parallelize this.
+    for (int i = 0; i < pendingConditionalRules.size(); i++) {
+      PendingConditionalRuleBase<T> rule = pendingConditionalRules.get(i);
+      if (rule != null && rule.isSatisfiedAfterUpdate(enqueuer)) {
+        ++prunedCount;
+        pendingConditionalRules.set(i, null);
+        enqueuer.includeMinimumKeepInfo(rule.getConsequences());
+        materializedRules.add(rule.asMaterialized());
+      }
+    }
+
+    if (prunedCount == pendingConditionalRules.size()) {
+      assert ListUtils.all(pendingConditionalRules, Objects::isNull);
+      prunedCount = 0;
+      pendingConditionalRules = Collections.emptyList();
+      return;
+    }
+
+    int threshold =
+        Math.max(reallocMinThreshold, pendingConditionalRules.size() / reallocRatioThreshold);
+    if (prunedCount >= threshold) {
+      int newSize = pendingConditionalRules.size() - prunedCount;
+      List<PendingConditionalRuleBase<T>> newPending = new ArrayList<>(newSize);
+      for (PendingConditionalRuleBase<T> rule : pendingConditionalRules) {
+        if (rule != null) {
+          assert rule.isOutstanding();
+          newPending.add(rule);
+        }
+      }
+      assert newPending.size() == newSize;
+      prunedCount = 0;
+      pendingConditionalRules = newPending;
+    }
+  }
+
+  @Override
+  public MaterializedRules getMaterializedRules() {
+    return new MaterializedRules(rootConsequences, materializedRules);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationFakeProguardRule.java b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationFakeProguardRule.java
new file mode 100644
index 0000000..2e28fcb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationFakeProguardRule.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.shaking.ProguardClassType;
+import com.android.tools.r8.shaking.ProguardKeepRuleBase;
+
+// TODO(b/323816623): Make an interface to use in the keep-reason tracking.
+public class KeepAnnotationFakeProguardRule extends ProguardKeepRuleBase {
+
+  public KeepAnnotationFakeProguardRule(KeepEdgeMetaInfo metaInfo) {
+    super(
+        Origin.unknown(),
+        Position.UNKNOWN,
+        "keep-annotation",
+        null,
+        null,
+        null,
+        false,
+        ProguardClassType.CLASS,
+        null,
+        null,
+        null,
+        false,
+        null,
+        null,
+        null);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java
new file mode 100644
index 0000000..ccac25a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcher.java
@@ -0,0 +1,491 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+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.graph.ProgramMember;
+import com.android.tools.r8.keepanno.ast.KeepBindingReference;
+import com.android.tools.r8.keepanno.ast.KeepBindings.KeepBindingSymbol;
+import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepCondition;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.Annotation;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.ClassInstantiate;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.ClassOpenHierarchy;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.FieldGet;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.FieldReplace;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.FieldSet;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.Lookup;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.MethodInvoke;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.MethodReplace;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.Name;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.NeverInline;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.VisibilityRelax;
+import com.android.tools.r8.keepanno.ast.KeepConstraint.VisibilityRestrict;
+import com.android.tools.r8.keepanno.ast.KeepConstraintVisitor;
+import com.android.tools.r8.keepanno.ast.KeepConstraints;
+import com.android.tools.r8.keepanno.ast.KeepDeclaration;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepItemReference;
+import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepTarget;
+import com.android.tools.r8.shaking.KeepInfo.Joiner;
+import com.android.tools.r8.shaking.MinimumKeepInfoCollection;
+import com.android.tools.r8.threading.ThreadingModule;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+
+public class KeepAnnotationMatcher {
+
+  public static ApplicableRulesEvaluator computeInitialRules(
+      AppInfoWithClassHierarchy appInfo,
+      List<KeepDeclaration> keepDeclarations,
+      ThreadingModule threadingModule,
+      ExecutorService executorService)
+      throws ExecutionException {
+    KeepAnnotationMatcherPredicates predicates =
+        new KeepAnnotationMatcherPredicates(appInfo.dexItemFactory());
+    ApplicableRulesEvaluator.Builder builder = ApplicableRulesEvaluator.initialRulesBuilder();
+    ThreadUtils.processItems(
+        keepDeclarations,
+        declaration -> processDeclaration(declaration, appInfo, predicates, builder),
+        threadingModule,
+        executorService);
+    return builder.build();
+  }
+
+  private static void processDeclaration(
+      KeepDeclaration declaration,
+      AppInfoWithClassHierarchy appInfo,
+      KeepAnnotationMatcherPredicates predicates,
+      ApplicableRulesEvaluator.Builder builder) {
+    EdgeMatcher edgeMatcher = new EdgeMatcher(appInfo, predicates);
+    declaration.match(
+        edge ->
+            edgeMatcher.forEachMatch(
+                edge,
+                result -> {
+                  if (result.preconditions.isEmpty()) {
+                    builder.addRootRule(
+                        keepInfoCollection -> createKeepInfo(result, keepInfoCollection));
+                  } else {
+                    builder.addConditionalRule(
+                        new PendingInitialConditionalRule(
+                            result.preconditions,
+                            createKeepInfo(result, MinimumKeepInfoCollection.create())));
+                  }
+                }),
+        check -> {
+          throw new Unimplemented();
+        });
+  }
+
+  private static MinimumKeepInfoCollection createKeepInfo(
+      MatchResult result, MinimumKeepInfoCollection minimumKeepInfoCollection) {
+    ListUtils.forEachWithIndex(
+        result.consequences,
+        (item, i) -> {
+          Joiner<?, ?, ?> joiner =
+              minimumKeepInfoCollection.getOrCreateMinimumKeepInfoFor(item.getReference());
+          updateWithConstraints(item, joiner, result.constraints.get(i), result.edge);
+        });
+    return minimumKeepInfoCollection;
+  }
+
+  private static void updateWithConstraints(
+      ProgramDefinition item, Joiner<?, ?, ?> joiner, KeepConstraints constraints, KeepEdge edge) {
+    constraints.forEachAccept(
+        new KeepConstraintVisitor() {
+
+          @Override
+          public void onLookup(Lookup constraint) {
+            joiner.disallowShrinking();
+            joiner.addRule(new KeepAnnotationFakeProguardRule(edge.getMetaInfo()));
+          }
+
+          @Override
+          public void onName(Name constraint) {
+            joiner.disallowMinification();
+            if (item.isProgramClass()) {
+              joiner.asClassJoiner().disallowRepackaging();
+            }
+          }
+
+          @Override
+          public void onVisibilityRelax(VisibilityRelax constraint) {
+            // R8 will never restrict the access, so this constraint is implicitly maintained.
+          }
+
+          @Override
+          public void onVisibilityRestrict(VisibilityRestrict constraint) {
+            joiner.disallowAccessModification();
+          }
+
+          @Override
+          public void onNeverInline(NeverInline constraint) {
+            joiner.disallowOptimization();
+          }
+
+          @Override
+          public void onClassInstantiate(ClassInstantiate constraint) {
+            joiner.disallowOptimization();
+          }
+
+          @Override
+          public void onClassOpenHierarchy(ClassOpenHierarchy constraint) {
+            joiner.disallowOptimization();
+          }
+
+          @Override
+          public void onMethodInvoke(MethodInvoke constraint) {
+            joiner.disallowOptimization();
+          }
+
+          @Override
+          public void onMethodReplace(MethodReplace constraint) {
+            joiner.disallowOptimization();
+          }
+
+          @Override
+          public void onFieldGet(FieldGet constraint) {
+            joiner.disallowOptimization();
+          }
+
+          @Override
+          public void onFieldSet(FieldSet constraint) {
+            joiner.disallowOptimization();
+          }
+
+          @Override
+          public void onFieldReplace(FieldReplace constraint) {
+            joiner.disallowOptimization();
+          }
+
+          @Override
+          public void onAnnotation(Annotation constraint) {
+            joiner.disallowAnnotationRemoval();
+          }
+        });
+  }
+
+  public static class MatchResult {
+    private final KeepEdge edge;
+    private final List<ProgramDefinition> preconditions;
+    private final List<ProgramDefinition> consequences;
+    private final List<KeepConstraints> constraints;
+
+    public MatchResult(
+        KeepEdge edge,
+        List<ProgramDefinition> preconditions,
+        List<ProgramDefinition> consequences,
+        List<KeepConstraints> constraints) {
+      this.edge = edge;
+      this.preconditions = preconditions;
+      this.consequences = consequences;
+      this.constraints = constraints;
+    }
+  }
+
+  public static class EdgeMatcher {
+
+    private final AppInfoWithClassHierarchy appInfo;
+    private final KeepAnnotationMatcherPredicates predicates;
+
+    private NormalizedSchema schema;
+    private Assignment assignment;
+    private Consumer<MatchResult> callback;
+
+    public EdgeMatcher(
+        AppInfoWithClassHierarchy appInfo, KeepAnnotationMatcherPredicates predicates) {
+      this.appInfo = appInfo;
+      this.predicates = predicates;
+    }
+
+    public void forEachMatch(KeepEdge edge, Consumer<MatchResult> callback) {
+      this.callback = callback;
+      schema = new NormalizedSchema(edge);
+      assignment = new Assignment(schema);
+      findMatchingClass(0);
+      schema = null;
+      assignment = null;
+    }
+
+    private void foundMatch() {
+      callback.accept(assignment.createMatch(schema));
+    }
+
+    private void findMatchingClass(int classIndex) {
+      if (classIndex == schema.classes.size()) {
+        // All classes and their members are assigned, so report the assignment.
+        foundMatch();
+        return;
+      }
+      KeepClassItemPattern classPattern = schema.classes.get(classIndex);
+      if (!classPattern.getClassNamePattern().isExact()) {
+        throw new Unimplemented();
+      }
+      DexType type =
+          appInfo
+              .dexItemFactory()
+              .createType(classPattern.getClassNamePattern().getExactDescriptor());
+      DexProgramClass clazz = DexProgramClass.asProgramClassOrNull(appInfo.definitionFor(type));
+      if (clazz == null) {
+        // No valid match, so the rule is discarded. This should likely be a diagnostics info.
+        return;
+      }
+      if (!predicates.matchesClass(clazz, classPattern)) {
+        // Invalid match for this class.
+        return;
+      }
+      assignment.setClass(classIndex, clazz);
+      IntList classMemberIndexList = schema.classMembers.get(classIndex);
+      findMatchingMember(0, classMemberIndexList, clazz, classIndex + 1);
+    }
+
+    private void findMatchingMember(
+        int memberInHolderIndex,
+        IntList memberIndexTranslation,
+        DexProgramClass holder,
+        int nextClassIndex) {
+      if (memberInHolderIndex == memberIndexTranslation.size()) {
+        // All members of this class are assigned, continue search for the next class.
+        findMatchingClass(nextClassIndex);
+        return;
+      }
+      int nextMemberInHolderIndex = memberInHolderIndex + 1;
+      int memberIndex = memberIndexTranslation.getInt(memberInHolderIndex);
+      KeepMemberItemPattern memberItemPattern = schema.members.get(memberIndex);
+      memberItemPattern
+          .getMemberPattern()
+          .match(
+              generalMember -> {
+                if (!holder.hasMethodsOrFields()) {
+                  // The empty class can only match the "all member" pattern but with no assignment.
+                  if (generalMember.isAllMembers()) {
+                    assignment.setEmptyMemberMatch(memberIndex);
+                    findMatchingMember(
+                        nextMemberInHolderIndex, memberIndexTranslation, holder, nextClassIndex);
+                  }
+                  return;
+                }
+                if (!generalMember.isAllMembers()) {
+                  throw new Unimplemented();
+                }
+                holder.forEachProgramMember(
+                    f -> {
+                      assignment.setMember(memberIndex, f);
+                      findMatchingMember(
+                          nextMemberInHolderIndex, memberIndexTranslation, holder, nextClassIndex);
+                    });
+              },
+              fieldMember -> {
+                throw new Unimplemented();
+              },
+              methodMember -> {
+                holder.forEachProgramMethod(
+                    m -> {
+                      if (predicates.matchesMethod(methodMember, m)) {
+                        assignment.setMember(memberIndex, m);
+                        findMatchingMember(
+                            nextMemberInHolderIndex,
+                            memberIndexTranslation,
+                            holder,
+                            nextClassIndex);
+                      }
+                    });
+              });
+    }
+  }
+
+  /**
+   * The normalized edge schema maps an edge into integer indexed class patterns and member
+   * patterns. The preconditions and consequences are then index references to these pattern. Each
+   * index denotes the identity of an item, thus the same reference must share the same item found
+   * by a pattern.
+   */
+  private static class NormalizedSchema {
+
+    final KeepEdge edge;
+    final Reference2IntMap<KeepBindingSymbol> symbolToKey = new Reference2IntOpenHashMap<>();
+    final List<KeepClassItemPattern> classes = new ArrayList<>();
+    final List<KeepMemberItemPattern> members = new ArrayList<>();
+    final List<IntList> classMembers = new ArrayList<>();
+    final IntList preconditions = new IntArrayList();
+    final IntList consequences = new IntArrayList();
+    final List<KeepConstraints> constraints = new ArrayList<>();
+
+    public NormalizedSchema(KeepEdge edge) {
+      this.edge = edge;
+      edge.getPreconditions().forEach(this::addPrecondition);
+      edge.getConsequences().forEachTarget(this::addConsequence);
+      ListUtils.forEachWithIndex(
+          members,
+          (member, memberIndex) -> {
+            member
+                .getClassReference()
+                .matchClassItemReference(
+                    bindingReference -> {
+                      int classIndex = symbolToKey.getInt(bindingReference);
+                      classMembers.get(classIndex).add(memberIndex);
+                    },
+                    classItemPattern -> {
+                      // The member declares its own inline class so link it directly.
+                      IntArrayList memberList = new IntArrayList();
+                      memberList.add(memberIndex);
+                      classes.add(classItemPattern);
+                      classMembers.add(memberList);
+                    });
+          });
+    }
+
+    public static boolean isClassKeyReference(int keyRef) {
+      return keyRef >= 0;
+    }
+
+    private int encodeClassKey(int key) {
+      assert isClassKeyReference(key);
+      return key;
+    }
+
+    public static int decodeClassKeyReference(int key) {
+      assert isClassKeyReference(key);
+      return key;
+    }
+
+    private int encodeMemberKey(int key) {
+      assert key >= 0;
+      return -(key + 1);
+    }
+
+    public static int decodeMemberKeyReference(int key) {
+      assert !isClassKeyReference(key);
+      assert key < 0;
+      return -(key + 1);
+    }
+
+    private int defineItemReference(KeepItemReference reference) {
+      return reference.apply(this::defineBindingReference, this::defineItemPattern);
+    }
+
+    private int defineBindingReference(KeepBindingReference reference) {
+      return symbolToKey.computeIfAbsent(
+          reference.getName(),
+          symbol -> defineItemPattern(edge.getBindings().get(symbol).getItem()));
+    }
+
+    private int defineItemPattern(KeepItemPattern item) {
+      if (item.isClassItemPattern()) {
+        int key = classes.size();
+        classes.add(item.asClassItemPattern());
+        classMembers.add(new IntArrayList());
+        return encodeClassKey(key);
+      }
+      int key = members.size();
+      members.add(item.asMemberItemPattern());
+      return encodeMemberKey(key);
+    }
+
+    public void addPrecondition(KeepCondition condition) {
+      preconditions.add(defineItemReference(condition.getItem()));
+    }
+
+    private void addConsequence(KeepTarget target) {
+      consequences.add(defineItemReference(target.getItem()));
+      constraints.add(target.getConstraints());
+    }
+  }
+
+  /**
+   * The assignment contains the full matching of the pattern, if a matching was found. The
+   * assignment is mutable and updated during the search. When a match is found the required
+   * information must be copied over immediately by creating a match result.
+   */
+  private static class Assignment {
+
+    final List<DexProgramClass> classes;
+    final List<ProgramMember<?, ?>> members;
+    boolean hasEmptyMembers = false;
+
+    private Assignment(NormalizedSchema schema) {
+      classes = Arrays.asList(new DexProgramClass[schema.classes.size()]);
+      members = Arrays.asList(new ProgramMember<?, ?>[schema.members.size()]);
+    }
+
+    ProgramDefinition getItemForReference(int keyReference) {
+      if (NormalizedSchema.isClassKeyReference(keyReference)) {
+        return classes.get(NormalizedSchema.decodeClassKeyReference(keyReference));
+      }
+      return members.get(NormalizedSchema.decodeMemberKeyReference(keyReference));
+    }
+
+    void setClass(int index, DexProgramClass type) {
+      classes.set(index, type);
+    }
+
+    void setMember(int index, ProgramMember<?, ?> member) {
+      members.set(index, member);
+    }
+
+    void setEmptyMemberMatch(int index) {
+      hasEmptyMembers = true;
+      members.set(index, null);
+    }
+
+    public MatchResult createMatch(NormalizedSchema schema) {
+      return new MatchResult(
+          schema.edge,
+          schema.preconditions.isEmpty()
+              ? Collections.emptyList()
+              : getItemList(schema.preconditions),
+          getItemList(schema.consequences),
+          getConstraints(schema));
+    }
+
+    private List<ProgramDefinition> getItemList(IntList indexReferences) {
+      assert !indexReferences.isEmpty();
+      List<ProgramDefinition> definitions = new ArrayList<>(indexReferences.size());
+      for (int i = 0; i < indexReferences.size(); i++) {
+        ProgramDefinition item = getItemForReference(indexReferences.getInt(i));
+        assert item != null || hasEmptyMembers;
+        if (item != null) {
+          definitions.add(item);
+        }
+      }
+      return definitions;
+    }
+
+    private List<KeepConstraints> getConstraints(NormalizedSchema schema) {
+      if (!hasEmptyMembers) {
+        return schema.constraints;
+      }
+      // Since members may have a matching "empty" pattern, we need to prune those from the
+      // constraints, so it matches the consequence list.
+      ImmutableList.Builder<KeepConstraints> builder = ImmutableList.builder();
+      for (int i = 0; i < schema.consequences.size(); i++) {
+        ProgramDefinition item = getItemForReference(schema.consequences.getInt(i));
+        if (item != null) {
+          builder.add(schema.constraints.get(i));
+        }
+      }
+      return builder.build();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java
new file mode 100644
index 0000000..45e8385
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/KeepAnnotationMatcherPredicates.java
@@ -0,0 +1,170 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.keepanno.ast.KeepArrayTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepInstanceOfPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodAccessPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepPrimitiveTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepStringPattern;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.OptionalPattern;
+import java.util.List;
+
+public class KeepAnnotationMatcherPredicates {
+
+  private final DexItemFactory factory;
+
+  public KeepAnnotationMatcherPredicates(DexItemFactory factory) {
+    this.factory = factory;
+  }
+
+  public boolean matchesClass(DexProgramClass clazz, KeepClassItemPattern classPattern) {
+    return matchesClassName(clazz.getType(), classPattern.getClassNamePattern())
+        && matchesInstanceOfPattern(clazz, classPattern.getInstanceOfPattern())
+        && matchesAnnotatedBy(clazz.annotations(), classPattern.getAnnotatedByPattern());
+  }
+
+  public boolean matchesClassName(DexType type, KeepQualifiedClassNamePattern pattern) {
+    if (pattern.isAny()) {
+      return true;
+    }
+    if (pattern.isExact()) {
+      return type.toDescriptorString().equals(pattern.getExactDescriptor());
+    }
+    throw new Unimplemented();
+  }
+
+  private boolean matchesInstanceOfPattern(
+      DexProgramClass unusedClazz, KeepInstanceOfPattern pattern) {
+    if (pattern.isAny()) {
+      return true;
+    }
+    throw new Unimplemented();
+  }
+
+  public boolean matchesMethod(KeepMethodPattern methodPattern, ProgramMethod method) {
+    if (methodPattern.isAnyMethod()) {
+      return true;
+    }
+    return matchesName(method.getName(), methodPattern.getNamePattern().asStringPattern())
+        && matchesReturnType(method.getReturnType(), methodPattern.getReturnTypePattern())
+        && matchesParameters(method.getParameters(), methodPattern.getParametersPattern())
+        && matchesAnnotatedBy(method.getAnnotations(), methodPattern.getAnnotatedByPattern())
+        && matchesAccess(method.getAccessFlags(), methodPattern.getAccessPattern());
+  }
+
+  public boolean matchesAccess(MethodAccessFlags access, KeepMethodAccessPattern pattern) {
+    if (pattern.isAny()) {
+      return true;
+    }
+    throw new Unimplemented();
+  }
+
+  public boolean matchesAnnotatedBy(
+      DexAnnotationSet annotations, OptionalPattern<KeepQualifiedClassNamePattern> pattern) {
+    if (pattern.isAbsent()) {
+      return true;
+    }
+    throw new Unimplemented();
+  }
+
+  public boolean matchesParameters(DexTypeList parameters, KeepMethodParametersPattern pattern) {
+    if (pattern.isAny()) {
+      return true;
+    }
+    List<KeepTypePattern> patternList = pattern.asList();
+    if (parameters.size() != patternList.size()) {
+      return false;
+    }
+    int size = parameters.size();
+    for (int i = 0; i < size; i++) {
+      if (!matchesType(parameters.get(i), patternList.get(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  public boolean matchesName(DexString name, KeepStringPattern namePattern) {
+    if (namePattern.isAny()) {
+      return true;
+    }
+    if (namePattern.isExact()) {
+      return namePattern.asExactString().equals(name.toString());
+    }
+    throw new Unimplemented();
+  }
+
+  public boolean matchesReturnType(
+      DexType returnType, KeepMethodReturnTypePattern returnTypePattern) {
+    if (returnTypePattern.isAny()) {
+      return true;
+    }
+    if (returnTypePattern.isVoid() && returnType.isVoidType()) {
+      return true;
+    }
+    return matchesType(returnType, returnTypePattern.asType());
+  }
+
+  public boolean matchesType(DexType type, KeepTypePattern pattern) {
+    return pattern.apply(
+        () -> true,
+        p -> matchesPrimitiveType(type, p),
+        p -> matchesArrayType(type, p),
+        p -> matchesClassType(type, p));
+  }
+
+  public boolean matchesClassType(DexType type, KeepQualifiedClassNamePattern pattern) {
+    if (!type.isClassType()) {
+      return false;
+    }
+    if (pattern.isAny()) {
+      return true;
+    }
+    if (pattern.isExact()) {
+      return pattern.getExactDescriptor().equals(type.toDescriptorString());
+    }
+    throw new Unimplemented();
+  }
+
+  public boolean matchesArrayType(DexType type, KeepArrayTypePattern pattern) {
+    if (!type.isArrayType()) {
+      return false;
+    }
+    if (pattern.isAny()) {
+      return true;
+    }
+    if (type.getArrayTypeDimensions() < pattern.getDimensions()) {
+      return false;
+    }
+    return matchesType(
+        type.toArrayElementAfterDimension(pattern.getDimensions(), factory), pattern.getBaseType());
+  }
+
+  public boolean matchesPrimitiveType(DexType type, KeepPrimitiveTypePattern pattern) {
+    if (!type.isPrimitiveType()) {
+      return false;
+    }
+    if (pattern.isAny()) {
+      return true;
+    }
+    return type.asPrimitiveTypeDescriptorChar() == pattern.getDescriptorChar();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/MaterializedConditionalRule.java b/src/main/java/com/android/tools/r8/shaking/rules/MaterializedConditionalRule.java
new file mode 100644
index 0000000..069b3c3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/MaterializedConditionalRule.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.shaking.MinimumKeepInfoCollection;
+import java.util.List;
+
+public class MaterializedConditionalRule {
+
+  private final List<DexReference> preconditions;
+  private final MinimumKeepInfoCollection consequences;
+
+  MaterializedConditionalRule(
+      List<DexReference> preconditions, MinimumKeepInfoCollection consequences) {
+    assert !preconditions.isEmpty();
+    this.preconditions = preconditions;
+    this.consequences = consequences;
+  }
+
+  PendingConditionalRule asPendingRule() {
+    return new PendingConditionalRule(preconditions, consequences);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/MaterializedRules.java b/src/main/java/com/android/tools/r8/shaking/rules/MaterializedRules.java
new file mode 100644
index 0000000..d12ce3d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/MaterializedRules.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
+import com.android.tools.r8.shaking.MinimumKeepInfoCollection;
+import com.android.tools.r8.utils.ListUtils;
+import java.util.Collections;
+import java.util.List;
+
+public class MaterializedRules {
+
+  private static final MaterializedRules EMPTY =
+      new MaterializedRules(MinimumKeepInfoCollection.empty(), Collections.emptyList());
+
+  private final MinimumKeepInfoCollection rootConsequences;
+  private final List<MaterializedConditionalRule> conditionalRules;
+
+  MaterializedRules(
+      MinimumKeepInfoCollection rootConsequences,
+      List<MaterializedConditionalRule> conditionalRules) {
+    this.rootConsequences = rootConsequences;
+    this.conditionalRules = conditionalRules;
+  }
+
+  public static MaterializedRules empty() {
+    return EMPTY;
+  }
+
+  public MaterializedRules rewriteWithLens(NonIdentityGraphLens lens) {
+    // The preconditions of these are not rewritten. We assume witnessing instructions
+    // to cary the original references to deem them effectively live.
+    // TODO(b/323816623): Do we need to rewrite the consequent sets? Or would the constraints
+    //  always ensure they remain if the keep info needs to be reapplied?
+    return this;
+  }
+
+  public ApplicableRulesEvaluator toApplicableRules() {
+    if (rootConsequences.isEmpty() && conditionalRules.isEmpty()) {
+      return ApplicableRulesEvaluator.empty();
+    }
+    return new ApplicableRulesEvaluatorImpl<>(
+        rootConsequences,
+        ListUtils.map(conditionalRules, MaterializedConditionalRule::asPendingRule));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/PendingConditionalRule.java b/src/main/java/com/android/tools/r8/shaking/rules/PendingConditionalRule.java
new file mode 100644
index 0000000..22d06ed
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/PendingConditionalRule.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.MinimumKeepInfoCollection;
+import java.util.List;
+
+public class PendingConditionalRule extends PendingConditionalRuleBase<DexReference> {
+
+  PendingConditionalRule(List<DexReference> preconditions, MinimumKeepInfoCollection consequences) {
+    super(preconditions, consequences);
+  }
+
+  @Override
+  DexReference getReference(DexReference item) {
+    return item;
+  }
+
+  @Override
+  boolean isSatisfied(DexReference item, Enqueuer enqueuer) {
+    return enqueuer.isOriginalReferenceEffectivelyLive(item);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/PendingConditionalRuleBase.java b/src/main/java/com/android/tools/r8/shaking/rules/PendingConditionalRuleBase.java
new file mode 100644
index 0000000..5716e74
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/PendingConditionalRuleBase.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.MinimumKeepInfoCollection;
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class PendingConditionalRuleBase<T> {
+
+  private final List<T> outstandingPreconditions;
+  private final List<DexReference> satisfiedPreconditions;
+  private final MinimumKeepInfoCollection consequences;
+
+  PendingConditionalRuleBase(List<T> preconditions, MinimumKeepInfoCollection consequences) {
+    this.outstandingPreconditions = preconditions;
+    this.satisfiedPreconditions = new ArrayList<>(preconditions.size());
+    this.consequences = consequences;
+  }
+
+  abstract DexReference getReference(T item);
+
+  abstract boolean isSatisfied(T item, Enqueuer enqueuer);
+
+  MinimumKeepInfoCollection getConsequences() {
+    return consequences;
+  }
+
+  MaterializedConditionalRule asMaterialized() {
+    assert !isOutstanding();
+    return new MaterializedConditionalRule(satisfiedPreconditions, consequences);
+  }
+
+  final boolean isOutstanding() {
+    return !outstandingPreconditions.isEmpty();
+  }
+
+  final boolean isSatisfiedAfterUpdate(Enqueuer enqueuer) {
+    assert isOutstanding();
+    int newlySatisfied = 0;
+    for (T precondition : outstandingPreconditions) {
+      if (isSatisfied(precondition, enqueuer)) {
+        ++newlySatisfied;
+        satisfiedPreconditions.add(getReference(precondition));
+      }
+    }
+    // Not satisfied and nothing changed.
+    if (newlySatisfied == 0) {
+      return false;
+    }
+    // The rule is satisfied in full.
+    if (newlySatisfied == outstandingPreconditions.size()) {
+      outstandingPreconditions.clear();
+      return true;
+    }
+    // Partially satisfied.
+    // This is expected to be the uncommon case so the update to outstanding is delayed to here.
+    outstandingPreconditions.removeIf(
+        outstanding -> satisfiedPreconditions.contains(getReference(outstanding)));
+    assert isOutstanding();
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/rules/PendingInitialConditionalRule.java b/src/main/java/com/android/tools/r8/shaking/rules/PendingInitialConditionalRule.java
new file mode 100644
index 0000000..8983f20
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/rules/PendingInitialConditionalRule.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.rules;
+
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.MinimumKeepInfoCollection;
+import java.util.List;
+
+public class PendingInitialConditionalRule extends PendingConditionalRuleBase<ProgramDefinition> {
+
+  PendingInitialConditionalRule(
+      List<ProgramDefinition> preconditions, MinimumKeepInfoCollection consequences) {
+    super(preconditions, consequences);
+    assert !preconditions.isEmpty();
+  }
+
+  @Override
+  DexReference getReference(ProgramDefinition item) {
+    return item.getReference();
+  }
+
+  @Override
+  boolean isSatisfied(ProgramDefinition item, Enqueuer enqueuer) {
+    if (item.isClass()) {
+      return enqueuer.isTypeLive(item.asClass());
+    }
+    if (item.isField()) {
+      return enqueuer.isFieldLive(item.asField());
+    }
+    assert item.isMethod();
+    return enqueuer.isMethodLive(item.asMethod());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index 22d19f7..26feadb 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -318,6 +318,15 @@
 
   public static <T> boolean all(List<T> items, Predicate<T> predicate) {
     for (T item : items) {
+      if (!predicate.test(item)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  public static <T> boolean any(List<T> items, Predicate<T> predicate) {
+    for (T item : items) {
       if (predicate.test(item)) {
         return true;
       }
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
index e9d6b40..9a3c1e8 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -104,6 +104,10 @@
     return this;
   }
 
+  public KeepAnnoTestBuilder enableNativeInterpretation() {
+    return this;
+  }
+
   public final KeepAnnoTestBuilder setExcludedOuterClass(Class<?> clazz) {
     return applyIfPG(b -> b.addDontWarn(clazz));
   }
@@ -175,14 +179,25 @@
       this.useEdgeExtraction = useEdgeExtraction;
       if (useEdgeExtraction) {
         builder.getBuilder().setEnableExperimentalKeepAnnotations(false);
-        builder.getBuilder().setEnableExperimentalVersionedKeepEdgeAnnotations(true);
+        builder.getBuilder().setEnableExperimentalExtractedKeepAnnotations(true);
       } else {
         builder.getBuilder().setEnableExperimentalKeepAnnotations(true);
-        builder.getBuilder().setEnableExperimentalVersionedKeepEdgeAnnotations(false);
+        builder.getBuilder().setEnableExperimentalExtractedKeepAnnotations(false);
       }
     }
 
     @Override
+    public KeepAnnoTestBuilder enableNativeInterpretation() {
+      if (useEdgeExtraction) {
+        // This enables native interpretation of the extracted edges.
+        builder.addOptionsModification(o -> o.testing.enableExtractedKeepAnnotations = true);
+        // This disables converting the extracted edges to PG rules in the command reader.
+        builder.getBuilder().setEnableExperimentalExtractedKeepAnnotations(false);
+      }
+      return this;
+    }
+
+    @Override
     public KeepAnnoTestBuilder applyIfR8(
         ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> builderConsumer) {
       builderConsumer.acceptWithRuntimeException(builder);
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java
index c1e5683..44306ed 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java
@@ -37,6 +37,7 @@
   @Test
   public void test() throws Exception {
     testForKeepAnno(parameters)
+        .enableNativeInterpretation()
         .addProgramClasses(getInputClasses())
         .addProgramClassFileData(
             transformer(B.class).removeMethods(MethodPredicate.all()).transform())