[KeepAnno] Add test support for extracting out keep edge annotations

Bug: b/323815449
Change-Id: Ia9dbb8db12d637d6a9f832859834b4c5ca82797e
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotations.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotations.java
new file mode 100644
index 0000000..43a91bb
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotations.java
@@ -0,0 +1,31 @@
+// 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.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface ExtractedKeepAnnotations {
+  /**
+   * The version of defining this extracted keep annotation.
+   *
+   * <p>Note: this version property must be the first property defined. Its content may determine
+   * the subsequent parsing.
+   */
+  String version();
+
+  /**
+   * The context giving rise to this extracted keep annotation.
+   *
+   * <p>The context must be a class descriptor, method descriptor or field descriptor.
+   */
+  String context();
+
+  /** The extracted edges. */
+  KeepEdge[] edges();
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index 1c6c71e..2d5d253 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Binding;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Condition;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.Extracted;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.FieldAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.ForApi;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Item;
@@ -35,6 +36,7 @@
 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.KeepEdgeException;
 import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
@@ -87,9 +89,19 @@
   public static int ASM_VERSION = ASM9;
 
   public static List<KeepDeclaration> readKeepEdges(byte[] classFileBytes) {
+    return internalReadKeepEdges(classFileBytes, false);
+  }
+
+  public static List<KeepDeclaration> readExtractedKeepEdges(byte[] classFileBytes) {
+    return internalReadKeepEdges(classFileBytes, true);
+  }
+
+  private static List<KeepDeclaration> internalReadKeepEdges(
+      byte[] classFileBytes, boolean onlyExtracted) {
     ClassReader reader = new ClassReader(classFileBytes);
     List<KeepDeclaration> declarations = new ArrayList<>();
-    reader.accept(new KeepEdgeClassVisitor(declarations::add), ClassReader.SKIP_CODE);
+    reader.accept(
+        new KeepEdgeClassVisitor(onlyExtracted, declarations::add), ClassReader.SKIP_CODE);
     return declarations;
   }
 
@@ -181,12 +193,14 @@
   }
 
   private static class KeepEdgeClassVisitor extends ClassVisitor {
+    private final boolean onlyExtracted;
     private final Parent<KeepDeclaration> parent;
     private String className;
     private ClassParsingContext parsingContext;
 
-    KeepEdgeClassVisitor(Parent<KeepDeclaration> parent) {
+    KeepEdgeClassVisitor(boolean onlyExtracted, Parent<KeepDeclaration> parent) {
       super(ASM_VERSION);
+      this.onlyExtracted = onlyExtracted;
       this.parent = parent;
     }
 
@@ -217,6 +231,18 @@
       if (visible) {
         return null;
       }
+      if (descriptor.equals(Extracted.DESCRIPTOR)) {
+        if (!onlyExtracted) {
+          // Annotation reading always ignores extracted edges.
+          // Note that we may reconsider this if R8 is to support a mixed-mode input.
+          return null;
+        }
+        return new ExtractedAnnotationVisitor(annotationParsingContext(descriptor), parent::accept);
+      }
+      if (onlyExtracted) {
+        // When reading extracted edges ignore all non-extracted annotations.
+        return null;
+      }
       if (descriptor.equals(Edge.DESCRIPTOR)) {
         return new KeepEdgeVisitor(
             annotationParsingContext(descriptor), parent::accept, this::setContext);
@@ -495,6 +521,86 @@
     }
   }
 
+  private static class ExtractedAnnotationVisitor extends AnnotationVisitorBase {
+
+    private final Parent<KeepDeclaration> parent;
+    private String context = null;
+    private String version = null;
+    private List<KeepEdgeVisitor> edgeVisitors = new ArrayList<>();
+
+    public ExtractedAnnotationVisitor(
+        AnnotationParsingContext parsingContext, Parent<KeepDeclaration> parent) {
+      super(parsingContext);
+      this.parent = parent;
+    }
+
+    private void ensureVersion(ParsingContext parsingContext) {
+      if (version == null) {
+        parsingContext.error("Property 'version' must be defined before any other property");
+      }
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (name.equals(Extracted.version) && value instanceof String) {
+        version = (String) value;
+        return;
+      }
+      ensureVersion(getParsingContext().property(name));
+      if (name.equals(Extracted.context) && value instanceof String) {
+        context = (String) value;
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(Extracted.edges)) {
+        PropertyParsingContext parsingContext = getParsingContext().property(name);
+        ensureVersion(parsingContext);
+        return new AnnotationVisitorBase(parsingContext) {
+          @Override
+          public AnnotationVisitor visitAnnotation(String nullName, String descriptor) {
+            assert nullName == null;
+            if (descriptor.equals(Edge.DESCRIPTOR)) {
+              KeepEdgeVisitor visitor =
+                  new KeepEdgeVisitor(
+                      parsingContext.annotation(descriptor), edge -> {}, builder -> {});
+              edgeVisitors.add(visitor);
+              return visitor;
+            }
+            return super.visitAnnotation(nullName, descriptor);
+          }
+        };
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      if (version == null) {
+        throw new KeepEdgeException("Invalid extracted edge, expected a version property.");
+      }
+      if (context == null) {
+        throw new KeepEdgeException("Invalid extracted edge, expected a context property.");
+      }
+      for (KeepEdgeVisitor visitor : edgeVisitors) {
+        parent.accept(
+            visitor
+                .builder
+                .setMetaInfo(
+                    visitor
+                        .metaInfoBuilder
+                        // TODO(b/323815449): This may be a method or field descriptor!
+                        .setContextFromClassDescriptor(context)
+                        .build())
+                .build());
+      }
+      super.visitEnd();
+    }
+  }
+
   private static class KeepEdgeVisitor extends AnnotationVisitorBase {
 
     private final ParsingContext parsingContext;
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
index 565067d..c822c51 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -3,26 +3,33 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.asm;
 
+import com.android.tools.r8.keepanno.ast.AccessVisibility;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.AnnotationPattern;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Condition;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.Extracted;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Item;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.MemberAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
 import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
-import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepEdgeMetaInfo;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepMemberAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
 import com.android.tools.r8.keepanno.ast.KeepMemberPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.ModifierPattern;
+import com.android.tools.r8.keepanno.ast.OptionalPattern;
 import com.android.tools.r8.keepanno.utils.Unimplemented;
 import java.util.function.BiFunction;
+import java.util.function.Consumer;
 import org.objectweb.asm.AnnotationVisitor;
-import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 
@@ -78,8 +85,26 @@
     };
   }
 
-  public static void writeEdge(KeepEdge edge, ClassVisitor visitor) {
-    writeEdgeInternal(edge, visitor::visitAnnotation);
+  // Helper to ensure that any call creating a new annotation visitor is statically scoped with its
+  // call to visit end.
+  private static void withNewVisitor(AnnotationVisitor visitor, Consumer<AnnotationVisitor> fn) {
+    fn.accept(visitor);
+    visitor.visitEnd();
+  }
+
+  public static void writeExtractedEdge(
+      KeepEdge edge, BiFunction<String, Boolean, AnnotationVisitorInterface> getVisitor) {
+    withNewVisitor(
+        wrap(getVisitor.apply(Extracted.DESCRIPTOR, false)),
+        extractVisitor -> {
+          extractVisitor.visit("version", edge.getMetaInfo().getVersion().toVersionString());
+          extractVisitor.visit("context", edge.getMetaInfo().getContextDescriptorString());
+          withNewVisitor(
+              extractVisitor.visitArray("edges"),
+              edgeVisitor ->
+                  writeEdgeInternal(
+                      edge, (desc, visible) -> edgeVisitor.visitAnnotation(null, desc)));
+        });
   }
 
   public static void writeEdge(
@@ -89,47 +114,55 @@
 
   public static void writeEdgeInternal(
       KeepEdge edge, BiFunction<String, Boolean, AnnotationVisitor> getVisitor) {
-    new KeepEdgeWriter().writeEdge(edge, getVisitor.apply(Edge.DESCRIPTOR, false));
+    withNewVisitor(
+        getVisitor.apply(Edge.DESCRIPTOR, false),
+        visitor -> new KeepEdgeWriter().writeEdge(edge, visitor));
   }
 
   private void writeEdge(KeepEdge edge, AnnotationVisitor visitor) {
+    writeMetaInfo(visitor, edge.getMetaInfo());
     writePreconditions(visitor, edge.getPreconditions());
     writeConsequences(visitor, edge.getConsequences());
-    visitor.visitEnd();
   }
 
+  private void writeMetaInfo(AnnotationVisitor visitor, KeepEdgeMetaInfo metaInfo) {}
+
   private void writePreconditions(AnnotationVisitor visitor, KeepPreconditions preconditions) {
     if (preconditions.isAlways()) {
       return;
     }
     String ignoredArrayValueName = null;
-    AnnotationVisitor arrayVisitor = visitor.visitArray(Edge.preconditions);
-    preconditions.forEach(
-        condition -> {
-          AnnotationVisitor conditionVisitor =
-              arrayVisitor.visitAnnotation(ignoredArrayValueName, Condition.DESCRIPTOR);
-          if (condition.getItem().isBindingReference()) {
-            throw new Unimplemented();
-          }
-          writeItem(conditionVisitor, condition.getItem().asItemPattern());
-        });
-    arrayVisitor.visitEnd();
+    withNewVisitor(
+        visitor.visitArray(Edge.preconditions),
+        arrayVisitor ->
+            preconditions.forEach(
+                condition ->
+                    withNewVisitor(
+                        arrayVisitor.visitAnnotation(ignoredArrayValueName, Condition.DESCRIPTOR),
+                        conditionVisitor -> {
+                          if (condition.getItem().isBindingReference()) {
+                            throw new Unimplemented();
+                          }
+                          writeItem(conditionVisitor, condition.getItem().asItemPattern());
+                        })));
   }
 
   private void writeConsequences(AnnotationVisitor visitor, KeepConsequences consequences) {
     assert !consequences.isEmpty();
     String ignoredArrayValueName = null;
-    AnnotationVisitor arrayVisitor = visitor.visitArray(Edge.consequences);
-    consequences.forEachTarget(
-        target -> {
-          AnnotationVisitor targetVisitor =
-              arrayVisitor.visitAnnotation(ignoredArrayValueName, Target.DESCRIPTOR);
-          if (target.getItem().isBindingReference()) {
-            throw new Unimplemented();
-          }
-          writeItem(targetVisitor, target.getItem().asItemPattern());
-        });
-    arrayVisitor.visitEnd();
+    withNewVisitor(
+        visitor.visitArray(Edge.consequences),
+        arrayVisitor ->
+            consequences.forEachTarget(
+                target ->
+                    withNewVisitor(
+                        arrayVisitor.visitAnnotation(ignoredArrayValueName, Target.DESCRIPTOR),
+                        targetVisitor -> {
+                          if (target.getItem().isBindingReference()) {
+                            throw new Unimplemented();
+                          }
+                          writeItem(targetVisitor, target.getItem().asItemPattern());
+                        })));
   }
 
   private void writeItem(AnnotationVisitor itemVisitor, KeepItemPattern item) {
@@ -145,7 +178,7 @@
     KeepQualifiedClassNamePattern namePattern = classItemPattern.getClassNamePattern();
     if (namePattern.isExact()) {
       Type typeConstant = Type.getType(namePattern.getExactDescriptor());
-      itemVisitor.visit(AnnotationConstants.Item.classConstant, typeConstant);
+      itemVisitor.visit(AnnotationConstants.Item.className, typeConstant.getClassName());
     } else {
       throw new Unimplemented();
     }
@@ -161,7 +194,6 @@
     }
     writeClassItem(memberItemPattern.getClassReference().asClassItemPattern(), itemVisitor);
     writeMember(memberItemPattern.getMemberPattern(), itemVisitor);
-    itemVisitor.visitEnd();
   }
 
   private void writeMember(KeepMemberPattern memberPattern, AnnotationVisitor targetVisitor) {
@@ -173,10 +205,19 @@
     } else if (memberPattern.isField()) {
       writeField(memberPattern.asField(), targetVisitor);
     } else {
-      throw new KeepEdgeException("Unexpected member pattern: " + memberPattern);
+      writeGeneralMember(memberPattern, targetVisitor);
     }
   }
 
+  private void writeGeneralMember(KeepMemberPattern member, AnnotationVisitor targetVisitor) {
+    assert member.isGeneralMember();
+    assert !member.isField();
+    assert !member.isMethod();
+    writeAnnotatedBy(
+        Item.memberAnnotatedByClassNamePattern, member.getAnnotatedByPattern(), targetVisitor);
+    writeAccessPattern(Item.memberAccess, member.getAccessPattern(), targetVisitor);
+  }
+
   private void writeField(KeepFieldPattern field, AnnotationVisitor targetVisitor) {
     String exactFieldName = field.getNamePattern().asExactString();
     if (exactFieldName != null) {
@@ -215,4 +256,74 @@
       throw new Unimplemented();
     }
   }
+
+  private void writeAnnotatedBy(
+      String propertyName,
+      OptionalPattern<KeepQualifiedClassNamePattern> annotatedByPattern,
+      AnnotationVisitor targetVisitor) {
+    if (annotatedByPattern.isAbsent()) {
+      return;
+    }
+    withNewVisitor(
+        targetVisitor.visitAnnotation(propertyName, AnnotationPattern.DESCRIPTOR),
+        visitor -> {
+          throw new Unimplemented("...");
+        });
+  }
+
+  private void writeModifierEnumValue(
+      ModifierPattern pattern, String value, String desc, AnnotationVisitor arrayVisitor) {
+    if (pattern.isAny()) {
+      return;
+    }
+    if (pattern.isOnlyPositive()) {
+      arrayVisitor.visitEnum(null, desc, value);
+    }
+    assert pattern.isOnlyNegative();
+    arrayVisitor.visitEnum(null, desc, MemberAccess.NEGATION_PREFIX + value);
+  }
+
+  private void writeAccessPattern(
+      String propertyName, KeepMemberAccessPattern accessPattern, AnnotationVisitor targetVisitor) {
+    if (accessPattern.isAny()) {
+      return;
+    }
+    withNewVisitor(
+        targetVisitor.visitArray(propertyName),
+        visitor -> {
+          if (!accessPattern.isAnyVisibility()) {
+            for (AccessVisibility visibility : accessPattern.getAllowedAccessVisibilities()) {
+              switch (visibility) {
+                case PUBLIC:
+                  visitor.visitEnum(null, MemberAccess.DESCRIPTOR, MemberAccess.PUBLIC);
+                  break;
+                case PROTECTED:
+                  visitor.visitEnum(null, MemberAccess.DESCRIPTOR, MemberAccess.PROTECTED);
+                  break;
+                case PACKAGE_PRIVATE:
+                  visitor.visitEnum(null, MemberAccess.DESCRIPTOR, MemberAccess.PACKAGE_PRIVATE);
+                  break;
+                case PRIVATE:
+                  visitor.visitEnum(null, MemberAccess.DESCRIPTOR, MemberAccess.PRIVATE);
+                  break;
+              }
+            }
+          }
+          writeModifierEnumValue(
+              accessPattern.getStaticPattern(),
+              MemberAccess.STATIC,
+              MemberAccess.DESCRIPTOR,
+              visitor);
+          writeModifierEnumValue(
+              accessPattern.getFinalPattern(),
+              MemberAccess.FINAL,
+              MemberAccess.DESCRIPTOR,
+              visitor);
+          writeModifierEnumValue(
+              accessPattern.getSyntheticPattern(),
+              MemberAccess.SYNTHETIC,
+              MemberAccess.DESCRIPTOR,
+              visitor);
+        });
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
index 18b2784..0789197 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
@@ -15,6 +15,14 @@
  * annotations which overlap in name with the actual semantic AST types.
  */
 public final class AnnotationConstants {
+  public static final class Extracted {
+    public static final String DESCRIPTOR =
+        "Lcom/android/tools/r8/keepanno/annotations/ExtractedKeepAnnotations;";
+    public static final String version = "version";
+    public static final String context = "context";
+    public static final String edges = "edges";
+  }
+
   public static final class Edge {
     public static final String DESCRIPTOR = "Lcom/android/tools/r8/keepanno/annotations/KeepEdge;";
     public static final String description = "description";
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepDeclaration.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepDeclaration.java
index bfcad37..c6f6248 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepDeclaration.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepDeclaration.java
@@ -6,6 +6,8 @@
 /** Base class for the declarations represented in the keep annoations library. */
 public abstract class KeepDeclaration {
 
+  public abstract KeepEdgeMetaInfo getMetaInfo();
+
   public final boolean isKeepEdge() {
     return asKeepEdge() != null;
   }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeMetaInfo.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeMetaInfo.java
index 4ec248b..6d4705a 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeMetaInfo.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeMetaInfo.java
@@ -7,8 +7,17 @@
 
 public class KeepEdgeMetaInfo {
 
+  public enum KeepEdgeVersion {
+    UNKNOWN;
+
+    public String toVersionString() {
+      return name();
+    }
+  }
+
   private static final KeepEdgeMetaInfo NONE =
-      new KeepEdgeMetaInfo(KeepEdgeContext.none(), KeepEdgeDescription.empty());
+      new KeepEdgeMetaInfo(
+          KeepEdgeVersion.UNKNOWN, KeepEdgeContext.none(), KeepEdgeDescription.empty());
 
   public static KeepEdgeMetaInfo none() {
     return NONE;
@@ -18,10 +27,13 @@
     return new Builder();
   }
 
+  private final KeepEdgeVersion version;
   private final KeepEdgeContext context;
   private final KeepEdgeDescription description;
 
-  private KeepEdgeMetaInfo(KeepEdgeContext context, KeepEdgeDescription description) {
+  private KeepEdgeMetaInfo(
+      KeepEdgeVersion version, KeepEdgeContext context, KeepEdgeDescription description) {
+    this.version = version;
     this.context = context;
     this.description = description;
   }
@@ -42,6 +54,14 @@
     return !KeepEdgeContext.none().equals(context);
   }
 
+  public boolean hasVersion() {
+    return version != KeepEdgeVersion.UNKNOWN;
+  }
+
+  public KeepEdgeVersion getVersion() {
+    return version;
+  }
+
   public static class Builder {
     private KeepEdgeContext context = KeepEdgeContext.none();
     private KeepEdgeDescription description = KeepEdgeDescription.empty();
@@ -73,7 +93,7 @@
           && description.equals(KeepEdgeDescription.empty())) {
         return none();
       }
-      return new KeepEdgeMetaInfo(context, description);
+      return new KeepEdgeMetaInfo(KeepEdgeVersion.UNKNOWN, context, description);
     }
   }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
index fd9cf63..57e8499 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
@@ -82,7 +82,11 @@
 
     @Override
     public String toString() {
-      return "Member{" + "annotated-by=" + annotatedByPattern + ", access=" + accessPattern + '}';
+      return "Member{"
+          + annotatedByPattern.mapOrDefault(p -> "@" + p + ", ", "")
+          + "access="
+          + accessPattern
+          + '}';
     }
   }
 
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/OptionalPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/OptionalPattern.java
index 18061b6..6a3832e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/OptionalPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/OptionalPattern.java
@@ -31,8 +31,12 @@
     throw new KeepEdgeException("Unexpected attempt to get absent value");
   }
 
-  public <S> OptionalPattern<S> map(Function<T, S> fn) {
-    return absent();
+  public final <S> OptionalPattern<S> map(Function<T, S> fn) {
+    return mapOrDefault(fn.andThen(OptionalPattern::of), absent());
+  }
+
+  public <S> S mapOrDefault(Function<T, S> fn, S defaultValue) {
+    return defaultValue;
   }
 
   private static final class Absent extends OptionalPattern {
@@ -52,6 +56,11 @@
     public int hashCode() {
       return System.identityHashCode(this);
     }
+
+    @Override
+    public String toString() {
+      return "<absent>";
+    }
   }
 
   private static final class Some<T> extends OptionalPattern<T> {
@@ -72,8 +81,8 @@
     }
 
     @Override
-    public <S> OptionalPattern<S> map(Function<T, S> fn) {
-      return of(fn.apply(value));
+    public <S> S mapOrDefault(Function<T, S> fn, S defaultValue) {
+      return fn.apply(value);
     }
 
     @Override
@@ -92,5 +101,10 @@
     public int hashCode() {
       return value.hashCode();
     }
+
+    @Override
+    public String toString() {
+      return value.toString();
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index c811323..ca3f045 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -137,6 +137,8 @@
     private boolean enableMissingLibraryApiModeling = false;
     private boolean enableExperimentalKeepAnnotations =
         System.getProperty("com.android.tools.r8.enableKeepAnnotations") != null;
+    private boolean enableExperimentalVersionedKeepEdgeAnnotations =
+        System.getProperty("com.android.tools.r8.enableVersionedKeepEdgeAnnotations") != null;
     public boolean enableStartupLayoutOptimization = true;
     private SemanticVersion fakeCompilerVersion = null;
     private AndroidResourceProvider androidResourceProvider = null;
@@ -504,6 +506,12 @@
       return self();
     }
 
+    @Deprecated
+    public Builder setEnableExperimentalVersionedKeepEdgeAnnotations(boolean enable) {
+      this.enableExperimentalVersionedKeepEdgeAnnotations = enable;
+      return self();
+    }
+
     @Override
     protected InternalProgramOutputPathConsumer createProgramOutputConsumer(
         Path path,
@@ -818,24 +826,31 @@
     }
 
     private void extractKeepAnnotationRules(ProguardConfigurationParser parser) {
-      if (!enableExperimentalKeepAnnotations) {
+      if (!enableExperimentalKeepAnnotations && !enableExperimentalVersionedKeepEdgeAnnotations) {
         return;
       }
+      assert enableExperimentalKeepAnnotations != enableExperimentalVersionedKeepEdgeAnnotations;
       try {
         for (ProgramResourceProvider provider : getAppBuilder().getProgramResourceProviders()) {
           for (ProgramResource resource : provider.getProgramResources()) {
             if (resource.getKind() == Kind.CF) {
-              List<KeepDeclaration> declarations =
-                  KeepEdgeReader.readKeepEdges(resource.getBytes());
-              KeepRuleExtractor extractor =
-                  new KeepRuleExtractor(
-                      rule -> {
-                        ProguardConfigurationSourceStrings source =
-                            new ProguardConfigurationSourceStrings(
-                                Collections.singletonList(rule), null, resource.getOrigin());
-                        parser.parse(source);
-                      });
-              declarations.forEach(extractor::extract);
+              List<KeepDeclaration> declarations;
+              if (!enableExperimentalKeepAnnotations) {
+                declarations = KeepEdgeReader.readExtractedKeepEdges(resource.getBytes());
+              } else {
+                declarations = KeepEdgeReader.readKeepEdges(resource.getBytes());
+              }
+              if (!declarations.isEmpty()) {
+                KeepRuleExtractor extractor =
+                    new KeepRuleExtractor(
+                        rule -> {
+                          ProguardConfigurationSourceStrings source =
+                              new ProguardConfigurationSourceStrings(
+                                  Collections.singletonList(rule), null, resource.getOrigin());
+                          parser.parse(source);
+                        });
+                declarations.forEach(extractor::extract);
+              }
             }
           }
         }
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 5beaaf2..8254a95 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -15,13 +15,24 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestShrinkerBuilder;
 import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
+import com.android.tools.r8.keepanno.asm.KeepEdgeWriter;
+import com.android.tools.r8.keepanno.ast.KeepDeclaration;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractorOptions;
+import com.android.tools.r8.keepanno.utils.Unimplemented;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.InternalOptions;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.function.Consumer;
 import org.junit.rules.TemporaryFolder;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
 
 public abstract class KeepAnnoTestBuilder {
 
@@ -109,6 +120,10 @@
     return inspectOutputConfig(System.out::println);
   }
 
+  public KeepAnnoTestBuilder enableEdgeExtraction() {
+    return this;
+  }
+
   public KeepAnnoTestBuilder inspectOutputConfig(Consumer<String> configConsumer) {
     // Default to ignore the consumer.
     return this;
@@ -151,6 +166,7 @@
 
     private final R8FullTestBuilder builder;
     private List<Consumer<R8TestCompileResult>> compileResultConsumers = new ArrayList<>();
+    private boolean useEdgeExtraction = false;
 
     public R8NativeBuilder(KeepAnnoParameters params, TemporaryFolder temp) {
       super(params, temp);
@@ -161,6 +177,14 @@
     }
 
     @Override
+    public KeepAnnoTestBuilder enableEdgeExtraction() {
+      useEdgeExtraction = true;
+      builder.getBuilder().setEnableExperimentalKeepAnnotations(false);
+      builder.getBuilder().setEnableExperimentalVersionedKeepEdgeAnnotations(true);
+      return this;
+    }
+
+    @Override
     public KeepAnnoTestBuilder applyIfR8(
         ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> builderConsumer) {
       builderConsumer.acceptWithRuntimeException(builder);
@@ -175,16 +199,19 @@
     }
 
     @Override
-    public KeepAnnoTestBuilder addProgramClasses(List<Class<?>> programClasses) {
-      builder.addProgramClasses(programClasses);
+    public KeepAnnoTestBuilder addProgramClasses(List<Class<?>> programClasses) throws IOException {
+      for (Class<?> programClass : programClasses) {
+        extractAndAdd(ToolHelper.getClassAsBytes(programClass));
+      }
       return this;
     }
 
     @Override
     public KeepAnnoTestBuilder addProgramClassFileData(List<byte[]> programClasses)
         throws IOException {
-
-      builder.addProgramClassFileData(programClasses);
+      for (byte[] programClass : programClasses) {
+        extractAndAdd(programClass);
+      }
       return this;
     }
 
@@ -195,6 +222,40 @@
       return this;
     }
 
+    private void extractAndAdd(byte[] classFileData) throws IOException {
+      builder.addProgramClassFileData(classFileData);
+      if (useEdgeExtraction) {
+        List<KeepDeclaration> declarations = KeepEdgeReader.readKeepEdges(classFileData);
+        if (!declarations.isEmpty()) {
+          String binaryName =
+              DescriptorUtils.getBinaryNameFromDescriptor(
+                  TestBase.extractClassDescriptor(classFileData));
+          String synthesizingTarget = binaryName + "$$ExtractedKeepEdges";
+          ClassWriter classWriter = new ClassWriter(InternalOptions.ASM_VERSION);
+          classWriter.visit(
+              Opcodes.V1_8,
+              ClassAccessFlags.createPublicFinalSynthetic().getAsCfAccessFlags(),
+              synthesizingTarget,
+              null,
+              "java/lang/Object",
+              null);
+          for (KeepDeclaration decl : declarations) {
+            if (!decl.isKeepEdge()) {
+              throw new Unimplemented("Support check declarations...");
+            } else {
+              KeepEdge edge = decl.asKeepEdge();
+              KeepEdgeWriter.writeExtractedEdge(
+                  edge,
+                  (descriptor, visible) ->
+                      KeepAnnoTestUtils.wrap(classWriter.visitAnnotation(descriptor, visible)));
+            }
+          }
+          classWriter.visitEnd();
+          builder.addProgramClassFileData(classWriter.toByteArray());
+        }
+      }
+    }
+
     @Override
     public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
       R8TestCompileResult compileResult = builder.compile();
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
index d15896f..534c5db 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
@@ -9,10 +9,12 @@
 import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
+import com.android.tools.r8.keepanno.asm.KeepEdgeWriter.AnnotationVisitorInterface;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractorOptions;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -22,6 +24,7 @@
 import java.util.List;
 import java.util.stream.Stream;
 import org.junit.rules.TemporaryFolder;
+import org.objectweb.asm.AnnotationVisitor;
 
 public class KeepAnnoTestUtils {
 
@@ -78,4 +81,47 @@
     }
     return rules;
   }
+
+  public static AnnotationVisitorInterface wrap(AnnotationVisitor visitor) {
+    return visitor == null ? null : new WrappedAnnotationVisitor(visitor);
+  }
+
+  private static class WrappedAnnotationVisitor implements AnnotationVisitorInterface {
+
+    private final AnnotationVisitor visitor;
+
+    private WrappedAnnotationVisitor(AnnotationVisitor visitor) {
+      this.visitor = visitor;
+    }
+
+    @Override
+    public int version() {
+      return InternalOptions.ASM_VERSION;
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      visitor.visit(name, value);
+    }
+
+    @Override
+    public void visitEnum(String name, String descriptor, String value) {
+      visitor.visitEnum(name, descriptor, value);
+    }
+
+    @Override
+    public AnnotationVisitorInterface visitAnnotation(String name, String descriptor) {
+      return wrap(visitor.visitAnnotation(name, descriptor));
+    }
+
+    @Override
+    public AnnotationVisitorInterface visitArray(String name) {
+      return wrap(visitor.visitArray(name));
+    }
+
+    @Override
+    public void visitEnd() {
+      visitor.visitEnd();
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepClassApiTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepClassApiTest.java
index 4abe376..378c929 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepClassApiTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepClassApiTest.java
@@ -59,6 +59,7 @@
     assertTrue(parameters.isShrinker());
     Box<Path> lib = new Box<>();
     testForKeepAnno(parameters)
+        .enableEdgeExtraction()
         .addProgramClasses(getLibraryClasses())
         .setExcludedOuterClass(getClass())
         .applyIfShrinker(b -> lib.set(b.compile().inspect(this::checkLibraryOutput).writeToZip()));
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
index aa675f2..977afd5 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
@@ -79,6 +79,8 @@
   private static final ClassReference USED_BY_NATIVE = annoClass("UsedByNative");
   private static final ClassReference CHECK_REMOVED = annoClass("CheckRemoved");
   private static final ClassReference CHECK_OPTIMIZED_OUT = annoClass("CheckOptimizedOut");
+  private static final ClassReference EXTRACTED_KEEP_ANNOTATIONS =
+      annoClass("ExtractedKeepAnnotations");
   private static final ClassReference KEEP_EDGE = annoClass("KeepEdge");
   private static final ClassReference KEEP_BINDING = annoClass("KeepBinding");
   private static final ClassReference KEEP_TARGET = annoClass("KeepTarget");
@@ -1583,6 +1585,7 @@
       withIndent(
           () -> {
             // Root annotations.
+            generateExtractedKeepAnnotationsConstants();
             generateKeepEdgeConstants();
             generateKeepForApiConstants();
             generateUsesReflectionConstants();
@@ -1615,6 +1618,28 @@
       println("public static final String DESCRIPTOR = " + quote(desc) + ";");
     }
 
+    private void generateExtractedKeepAnnotationsConstants() {
+      println("public static final class Extracted {");
+      withIndent(
+          () -> {
+            generateAnnotationConstants(EXTRACTED_KEEP_ANNOTATIONS);
+            new GroupMember("version")
+                .setDocTitle("Extraction version used to generate this keep annotation.")
+                .requiredStringValue()
+                .generateConstants(this);
+            new GroupMember("context")
+                .setDocTitle("Extraction context from which this keep annotation is generated.")
+                .requiredStringValue()
+                .generateConstants(this);
+            new GroupMember("edges")
+                .setDocTitle("Extracted normalized keep edges.")
+                .requiredArrayValue(KEEP_EDGE)
+                .generateConstants(this);
+          });
+      println("}");
+      println();
+    }
+
     private void generateKeepEdgeConstants() {
       println("public static final class Edge {");
       withIndent(