Builder based AST for keep edge descriptions.

Bug: b/248408342
Change-Id: I5ba73c0e5de47506d01f617b97dd61a96b46e0c2
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java
new file mode 100644
index 0000000..318ab92
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java
@@ -0,0 +1,81 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+/**
+ * A keep condition is the content of an item in the set of preconditions.
+ *
+ * <p>It can be trivially true, or represent some program item pattern that must be present in the
+ * program residual. When an condition is denoted by a program item, the condition also specifies
+ * the extent of the item for which it is predicated on. The extent is given by a "usage kind" that
+ * can be either its "symbolic reference" or its "actual use".
+ */
+public abstract class KeepCondition {
+
+  /** A condition that is unconditionally true. */
+  public static KeepCondition trueCondition() {
+    return KeepConditionTrue.getInstance();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    private KeepUsageKind usageKind = KeepUsageKind.symbolicReference();
+    private KeepItemPattern itemPattern;
+
+    private Builder() {}
+
+    public Builder setUsageKind(KeepUsageKind usageKind) {
+      this.usageKind = usageKind;
+      return this;
+    }
+
+    public Builder setItem(KeepItemPattern itemPattern) {
+      this.itemPattern = itemPattern;
+      return this;
+    }
+
+    public KeepCondition build() {
+      return new KeepConditionItem(itemPattern);
+    }
+  }
+
+  private static class KeepConditionTrue extends KeepCondition {
+
+    private static KeepConditionTrue INSTANCE = null;
+
+    public static KeepConditionTrue getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepConditionTrue();
+      }
+      return INSTANCE;
+    }
+  }
+
+  private static class KeepConditionFalse extends KeepCondition {
+
+    private static KeepConditionFalse INSTANCE = null;
+
+    public static KeepConditionFalse getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepConditionFalse();
+      }
+      return INSTANCE;
+    }
+  }
+
+  private static class KeepConditionItem extends KeepCondition {
+
+    private final KeepItemPattern itemPattern;
+
+    private KeepConditionItem(KeepItemPattern itemPattern) {
+      this.itemPattern = itemPattern;
+    }
+  }
+
+  private KeepCondition() {}
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java
new file mode 100644
index 0000000..764d734
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Set of consequences of a keep edge.
+ *
+ * <p>The consequences are "targets" described by item patterns along with "keep options" which
+ * detail what aspects of the items must be retained.
+ *
+ * <p>The consequences come into effect if the preconditions of an edge are met.
+ */
+public final class KeepConsequences {
+
+  public static class Builder {
+
+    private List<KeepTarget> targets = new ArrayList<>();
+
+    private Builder() {}
+
+    public Builder addTarget(KeepTarget target) {
+      targets.add(target);
+      return this;
+    }
+
+    public KeepConsequences build() {
+      return new KeepConsequences(targets);
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private final List<KeepTarget> targets;
+
+  private KeepConsequences(List<KeepTarget> targets) {
+    this.targets = targets;
+  }
+
+  public boolean isEmpty() {
+    return targets.isEmpty();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java
new file mode 100644
index 0000000..d85f1e8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java
@@ -0,0 +1,49 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+/**
+ * An edge in the keep graph.
+ *
+ * <p>An edge describes a set of preconditions and a set of consequences. If the preconditions are
+ * met, then the consequences are put into effect.
+ */
+public final class KeepEdge {
+
+  public static class Builder {
+    private KeepPreconditions preconditions = KeepPreconditions.always();
+    private KeepConsequences consequences;
+
+    private Builder() {}
+
+    public Builder setPreconditions(KeepPreconditions preconditions) {
+      this.preconditions = preconditions;
+      return this;
+    }
+
+    public Builder setConsequences(KeepConsequences consequences) {
+      this.consequences = consequences;
+      return this;
+    }
+
+    public KeepEdge build() {
+      if (consequences.isEmpty()) {
+        throw new KeepEdgeException("KeepEdge must have non-empty set of consequences.");
+      }
+      return new KeepEdge(preconditions, consequences);
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private final KeepPreconditions precondition;
+  private final KeepConsequences consequences;
+
+  private KeepEdge(KeepPreconditions precondition, KeepConsequences consequences) {
+    this.precondition = precondition;
+    this.consequences = consequences;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java
new file mode 100644
index 0000000..3345c8f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public class KeepEdgeException extends RuntimeException {
+
+  public KeepEdgeException(String message) {
+    super(message);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java
new file mode 100644
index 0000000..af7c35f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+/** Pattern for matching the "extends" or "implements" clause of a class. */
+public abstract class KeepExtendsPattern {
+
+  public static KeepExtendsPattern any() {
+    return KeepExtendsAnyPattern.getInstance();
+  }
+
+  public static class Builder {
+
+    private KeepExtendsPattern pattern;
+
+    private Builder() {}
+
+    public Builder any() {
+      pattern = KeepExtendsAnyPattern.getInstance();
+      return this;
+    }
+
+    public Builder classPattern(KeepQualifiedClassNamePattern pattern) {
+      this.pattern = new KeepExtendsClassPattern(pattern);
+      return this;
+    }
+  }
+
+  private static class KeepExtendsAnyPattern extends KeepExtendsPattern {
+
+    private static KeepExtendsAnyPattern INSTANCE = null;
+
+    public static KeepExtendsAnyPattern getInstance() {
+      return INSTANCE;
+    }
+
+    @Override
+    public boolean isAny() {
+      return true;
+    }
+  }
+
+  private static class KeepExtendsClassPattern extends KeepExtendsPattern {
+
+    private final KeepQualifiedClassNamePattern pattern;
+
+    public KeepExtendsClassPattern(KeepQualifiedClassNamePattern pattern) {
+      this.pattern = pattern;
+    }
+
+    @Override
+    public boolean isAny() {
+      return pattern.isAny();
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private KeepExtendsPattern() {}
+
+  public abstract boolean isAny();
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java
new file mode 100644
index 0000000..04e2697
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public class KeepFieldPattern extends KeepMemberPattern {
+
+  private KeepFieldPattern() {}
+
+  public boolean isAnyField() {
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java
new file mode 100644
index 0000000..c994aa5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+/**
+ * A pattern for matching items in the program.
+ *
+ * <p>An item pattern can be any item, or it can describe a family of classes or a family of members
+ * on a classes.
+ *
+ * <p>A pattern cannot describe both a class *and* a member of a class. Either it is a pattern on
+ * classes or it is a pattern on members. The distinction is defined by having a "none" member
+ * pattern.
+ */
+public abstract class KeepItemPattern {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static KeepItemPattern any() {
+    return KeepItemAnyPattern.getInstance();
+  }
+
+  public static class Builder {
+
+    private KeepQualifiedClassNamePattern classNamePattern;
+    private KeepExtendsPattern extendsPattern = KeepExtendsPattern.any();
+    private KeepMembersPattern membersPattern = KeepMembersPattern.none();
+
+    private Builder() {}
+
+    public Builder any() {
+      classNamePattern = KeepQualifiedClassNamePattern.any();
+      extendsPattern = KeepExtendsPattern.any();
+      membersPattern = KeepMembersPattern.all();
+      return this;
+    }
+
+    public Builder setClassPattern(KeepQualifiedClassNamePattern qualifiedClassNamePattern) {
+      this.classNamePattern = qualifiedClassNamePattern;
+      return this;
+    }
+
+    public Builder setExtendsPattern(KeepExtendsPattern extendsPattern) {
+      this.extendsPattern = extendsPattern;
+      return this;
+    }
+
+    public Builder setMembersPattern(KeepMembersPattern membersPattern) {
+      this.membersPattern = membersPattern;
+      return this;
+    }
+
+    public KeepItemPattern build() {
+      if (classNamePattern == null) {
+        throw new KeepEdgeException("Class pattern must define a class name pattern.");
+      }
+      if (classNamePattern.isAny() && extendsPattern.isAny() && membersPattern.isAll()) {
+        return KeepItemPattern.any();
+      }
+      return new KeepClassPattern(classNamePattern, extendsPattern, membersPattern);
+    }
+  }
+
+  private static class KeepItemAnyPattern extends KeepItemPattern {
+
+    private static KeepItemAnyPattern INSTANCE = null;
+
+    public static KeepItemAnyPattern getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepItemAnyPattern();
+      }
+      return INSTANCE;
+    }
+  }
+
+  private static class KeepClassPattern extends KeepItemPattern {
+
+    private final KeepQualifiedClassNamePattern qualifiedClassPattern;
+    private final KeepExtendsPattern extendsPattern;
+    private final KeepMembersPattern membersPattern;
+    // TODO: class annotations
+
+    private KeepClassPattern(
+        KeepQualifiedClassNamePattern qualifiedClassPattern,
+        KeepExtendsPattern extendsPattern,
+        KeepMembersPattern membersPattern) {
+      this.qualifiedClassPattern = qualifiedClassPattern;
+      this.extendsPattern = extendsPattern;
+      this.membersPattern = membersPattern;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java
new file mode 100644
index 0000000..73bae7e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public abstract class KeepMemberPattern {
+
+  public static KeepMemberPattern anyMember() {
+    return KeepMemberAnyPattern.getInstance();
+  }
+
+  private static class KeepMemberAnyPattern extends KeepMemberPattern {
+    private static KeepMemberAnyPattern INSTANCE = null;
+
+    public static KeepMemberAnyPattern getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepMemberAnyPattern();
+      }
+      return INSTANCE;
+    }
+
+    @Override
+    public boolean isAnyMember() {
+      return true;
+    }
+  }
+
+  public boolean isAnyMember() {
+    return false;
+  }
+
+  abstract static class Builder<T extends Builder<T>> {
+
+    public abstract T self();
+
+    Builder() {}
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java
new file mode 100644
index 0000000..cccab80
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java
@@ -0,0 +1,138 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class KeepMembersPattern {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static KeepMembersPattern none() {
+    return KeepMembersNonePattern.getInstance();
+  }
+
+  public static KeepMembersPattern all() {
+    return KeepMembersAllPattern.getInstance();
+  }
+
+  public static class Builder {
+
+    private boolean anyMethod = false;
+    private boolean anyField = false;
+    private List<KeepMethodPattern> methods = new ArrayList<>();
+    private List<KeepFieldPattern> fields = new ArrayList<>();
+
+    public Builder addMethodPattern(KeepMethodPattern methodPattern) {
+      if (anyMethod) {
+        return this;
+      }
+      if (methodPattern.isAnyMethod()) {
+        methods.clear();
+        anyMethod = true;
+      }
+      methods.add(methodPattern);
+      return this;
+    }
+
+    public Builder addFieldPattern(KeepFieldPattern fieldPattern) {
+      if (anyField) {
+        return this;
+      }
+      if (fieldPattern.isAnyField()) {
+        fields.clear();
+        anyField = true;
+      }
+      fields.add(fieldPattern);
+      return this;
+    }
+
+    public KeepMembersPattern build() {
+      if (methods.isEmpty() && fields.isEmpty()) {
+        return KeepMembersPattern.none();
+      }
+      if (anyMethod && anyField) {
+        return KeepMembersPattern.all();
+      }
+      return new KeepMembersSomePattern(methods, fields);
+    }
+  }
+
+  private static class KeepMembersAllPattern extends KeepMembersPattern {
+
+    private static KeepMembersAllPattern INSTANCE = null;
+
+    public static KeepMembersAllPattern getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepMembersAllPattern();
+      }
+      return INSTANCE;
+    }
+
+    @Override
+    public boolean isAll() {
+      return true;
+    }
+
+    @Override
+    public boolean isNone() {
+      return true;
+    }
+  }
+
+  private static class KeepMembersNonePattern extends KeepMembersPattern {
+
+    private static KeepMembersNonePattern INSTANCE = null;
+
+    public static KeepMembersNonePattern getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepMembersNonePattern();
+      }
+      return INSTANCE;
+    }
+
+    @Override
+    public boolean isAll() {
+      return false;
+    }
+
+    @Override
+    public boolean isNone() {
+      return true;
+    }
+  }
+
+  private static class KeepMembersSomePattern extends KeepMembersPattern {
+
+    private final List<KeepMethodPattern> methods;
+    private final List<KeepFieldPattern> fields;
+
+    private KeepMembersSomePattern(List<KeepMethodPattern> methods, List<KeepFieldPattern> fields) {
+      assert !methods.isEmpty() || !fields.isEmpty();
+      this.methods = methods;
+      this.fields = fields;
+    }
+
+    @Override
+    public boolean isAll() {
+      // Since there is at least one none-all field or method this is not a match all.
+      return false;
+    }
+
+    @Override
+    public boolean isNone() {
+      // Since there is at least one field or method this is not a match none.
+      return false;
+    }
+  }
+
+  private KeepMembersPattern() {}
+
+  public abstract boolean isAll();
+
+  public abstract boolean isNone();
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java
new file mode 100644
index 0000000..141046a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java
@@ -0,0 +1,38 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public class KeepMethodNamePattern {
+
+  public static KeepMethodNamePattern any() {
+    return KeepMethodNameAnyPattern.getInstance();
+  }
+
+  public static KeepMethodNamePattern initializer() {
+    return new KeepMethodNameExactPattern("<init>");
+  }
+
+  public static KeepMethodNamePattern exact(String methodName) {
+    return new KeepMethodNameExactPattern(methodName);
+  }
+
+  private static class KeepMethodNameAnyPattern extends KeepMethodNamePattern {
+    private static KeepMethodNameAnyPattern INSTANCE = null;
+
+    public static KeepMethodNameAnyPattern getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepMethodNameAnyPattern();
+      }
+      return INSTANCE;
+    }
+  }
+
+  private static class KeepMethodNameExactPattern extends KeepMethodNamePattern {
+    private final String name;
+
+    public KeepMethodNameExactPattern(String name) {
+      this.name = name;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java
new file mode 100644
index 0000000..66de66d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public abstract class KeepMethodParametersPattern {
+
+  public static KeepMethodParametersPattern any() {
+    return Any.getInstance();
+  }
+
+  public static KeepMethodParametersPattern none() {
+    return None.getInstance();
+  }
+
+  private static class None extends KeepMethodParametersPattern {
+    private static None INSTANCE = null;
+
+    public static None getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new None();
+      }
+      return INSTANCE;
+    }
+  }
+
+  private static class Any extends KeepMethodParametersPattern {
+    private static Any INSTANCE = null;
+
+    public static Any getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new Any();
+      }
+      return INSTANCE;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java
new file mode 100644
index 0000000..8ac3c82
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java
@@ -0,0 +1,64 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public final class KeepMethodPattern extends KeepMemberPattern {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends KeepMemberPattern.Builder<Builder> {
+
+    private KeepMethodNamePattern namePattern = null;
+    private KeepMethodReturnTypePattern returnTypePattern = KeepMethodReturnTypePattern.any();
+    private KeepMethodParametersPattern parametersPattern = KeepMethodParametersPattern.any();
+
+    private Builder() {}
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+
+    public Builder setNamePattern(KeepMethodNamePattern namePattern) {
+      this.namePattern = namePattern;
+      return self();
+    }
+
+    public Builder setReturnTypeVoid() {
+      returnTypePattern = KeepMethodReturnTypePattern.voidType();
+      return self();
+    }
+
+    public Builder setParametersPattern(KeepMethodParametersPattern parametersPattern) {
+      this.parametersPattern = parametersPattern;
+      return self();
+    }
+
+    public KeepMethodPattern build() {
+      if (namePattern == null) {
+        throw new KeepEdgeException("Method pattern must declar a name pattern");
+      }
+      return new KeepMethodPattern(namePattern, returnTypePattern, parametersPattern);
+    }
+  }
+
+  private final KeepMethodNamePattern namePattern;
+  private final KeepMethodReturnTypePattern returnTypePattern;
+  private final KeepMethodParametersPattern parametersPattern;
+
+  private KeepMethodPattern(
+      KeepMethodNamePattern namePattern,
+      KeepMethodReturnTypePattern returnTypePattern,
+      KeepMethodParametersPattern parametersPattern) {
+    this.namePattern = namePattern;
+    this.returnTypePattern = returnTypePattern;
+    this.parametersPattern = parametersPattern;
+  }
+
+  public boolean isAnyMethod() {
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java
new file mode 100644
index 0000000..acad630
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public class KeepMethodReturnTypePattern {
+
+  public static KeepMethodReturnTypePattern any() {
+    return Any.getInstance();
+  }
+
+  public static KeepMethodReturnTypePattern voidType() {
+    return VoidType.getInstance();
+  }
+
+  private static class VoidType extends KeepMethodReturnTypePattern {
+    private static VoidType INSTANCE = null;
+
+    public static VoidType getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new VoidType();
+      }
+      return INSTANCE;
+    }
+  }
+
+  private static class Any extends KeepMethodReturnTypePattern {
+    private static Any INSTANCE = null;
+
+    public static Any getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new Any();
+      }
+      return INSTANCE;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java
new file mode 100644
index 0000000..3c3aa11
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+import java.util.Collections;
+import java.util.Set;
+
+public final class KeepOptions {
+
+  public enum KeepOption {
+    SHRINKING,
+    OPTIMIZING,
+    OBFUSCATING
+  }
+
+  public static KeepOptions keepAll() {
+    if (ALLOW_NONE_INSTANCE == null) {
+      ALLOW_NONE_INSTANCE = new KeepOptions(true, Collections.emptySet());
+    }
+    return ALLOW_NONE_INSTANCE;
+  }
+
+  private static KeepOptions ALLOW_NONE_INSTANCE = null;
+
+  private final boolean allowIfSet;
+  private final Set<KeepOption> options;
+
+  private KeepOptions(boolean allowIfSet, Set<KeepOption> options) {
+    this.allowIfSet = allowIfSet;
+    this.options = options;
+  }
+
+  public boolean allow(KeepOption option) {
+    return options.contains(option) == allowIfSet;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java
new file mode 100644
index 0000000..e5353c0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public abstract class KeepPackagePattern {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static KeepPackagePattern any() {
+    return KeepPackageAnyPattern.getInstance();
+  }
+
+  public static KeepPackagePattern top() {
+    return KeepPackageTopPattern.getInstance();
+  }
+
+  public static KeepPackagePattern exact(String fullPackage) {
+    return KeepPackagePattern.builder().exact(fullPackage).build();
+  }
+
+  public static class Builder {
+
+    private KeepPackagePattern pattern;
+
+    public Builder any() {
+      pattern = KeepPackageAnyPattern.getInstance();
+      return this;
+    }
+
+    public Builder top() {
+      pattern = KeepPackageTopPattern.getInstance();
+      return this;
+    }
+
+    public Builder exact(String fullPackage) {
+      pattern =
+          fullPackage.isEmpty()
+              ? KeepPackagePattern.top()
+              : new KeepPackageExactPattern(fullPackage);
+      return this;
+    }
+
+    public KeepPackagePattern build() {
+      if (pattern == null) {
+        throw new KeepEdgeException("Invalid package pattern: null");
+      }
+      return pattern;
+    }
+  }
+
+  private static final class KeepPackageAnyPattern extends KeepPackagePattern {
+
+    private static KeepPackageAnyPattern INSTANCE = null;
+
+    public static KeepPackageAnyPattern getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepPackageAnyPattern();
+      }
+      return INSTANCE;
+    }
+
+    private KeepPackageAnyPattern() {}
+
+    @Override
+    public boolean isAny() {
+      return true;
+    }
+  }
+
+  private static final class KeepPackageTopPattern extends KeepPackagePattern {
+
+    private static KeepPackageTopPattern INSTANCE = null;
+
+    public static KeepPackageTopPattern getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepPackageTopPattern();
+      }
+      return INSTANCE;
+    }
+
+    private KeepPackageTopPattern() {}
+
+    @Override
+    public boolean isAny() {
+      return false;
+    }
+  }
+
+  private static final class KeepPackageExactPattern extends KeepPackagePattern {
+
+    private final String fullPackage;
+
+    private KeepPackageExactPattern(String fullPackage) {
+      this.fullPackage = fullPackage;
+      // TODO: Verify valid package identifiers.
+    }
+
+    @Override
+    public boolean isAny() {
+      return false;
+    }
+  }
+
+  public abstract boolean isAny();
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java
new file mode 100644
index 0000000..ea347b4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class KeepPreconditions {
+
+  private static class KeepPreconditionsAlways extends KeepPreconditions {
+
+    private static KeepPreconditionsAlways INSTANCE = null;
+
+    public static KeepPreconditionsAlways getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepPreconditionsAlways();
+      }
+      return INSTANCE;
+    }
+  }
+
+  private static class KeepPreconditionsSome extends KeepPreconditions {
+
+    private final List<KeepCondition> preconditions;
+
+    private KeepPreconditionsSome(List<KeepCondition> preconditions) {
+      this.preconditions = preconditions;
+    }
+  }
+
+  public static class Builder {
+
+    private List<KeepCondition> preconditions = new ArrayList<>();
+
+    private Builder() {}
+
+    public Builder addCondition(KeepCondition condition) {
+      preconditions.add(condition);
+      return this;
+    }
+
+    public KeepPreconditions build() {
+      return new KeepPreconditionsSome(preconditions);
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static KeepPreconditions always() {
+    return KeepPreconditionsAlways.getInstance();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java
new file mode 100644
index 0000000..5a9151f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public class KeepQualifiedClassNamePattern {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static KeepQualifiedClassNamePattern any() {
+    return KeepQualifiedClassNamePattern.builder()
+        .setPackagePattern(KeepPackagePattern.any())
+        .setNamePattern(KeepUnqualfiedClassNamePattern.any())
+        .build();
+  }
+
+  public static KeepQualifiedClassNamePattern exact(String qualifiedClassName) {
+    int pkgSeparator = qualifiedClassName.lastIndexOf('.');
+    if (pkgSeparator == 0) {
+      throw new KeepEdgeException("Unexpected '.' at index 0 in '" + qualifiedClassName + "'");
+    }
+    if (pkgSeparator > 0) {
+      return KeepQualifiedClassNamePattern.builder()
+          .setPackagePattern(
+              KeepPackagePattern.exact(qualifiedClassName.substring(0, pkgSeparator)))
+          .setNamePattern(
+              KeepUnqualfiedClassNamePattern.exact(qualifiedClassName.substring(pkgSeparator + 1)))
+          .build();
+    }
+    return KeepQualifiedClassNamePattern.builder()
+        .setPackagePattern(KeepPackagePattern.top())
+        .setNamePattern(KeepUnqualfiedClassNamePattern.exact(qualifiedClassName))
+        .build();
+  }
+
+  public static class Builder {
+
+    private KeepPackagePattern packagePattern;
+    private KeepUnqualfiedClassNamePattern namePattern;
+
+    private Builder() {}
+
+    public Builder setPackagePattern(KeepPackagePattern packagePattern) {
+      this.packagePattern = packagePattern;
+      return this;
+    }
+
+    public Builder setNamePattern(KeepUnqualfiedClassNamePattern namePattern) {
+      this.namePattern = namePattern;
+      return this;
+    }
+
+    public KeepQualifiedClassNamePattern build() {
+      return new KeepQualifiedClassNamePattern(packagePattern, namePattern);
+    }
+  }
+
+  private final KeepPackagePattern packagePattern;
+  private final KeepUnqualfiedClassNamePattern namePattern;
+
+  public KeepQualifiedClassNamePattern(
+      KeepPackagePattern packagePattern, KeepUnqualfiedClassNamePattern namePattern) {
+    this.packagePattern = packagePattern;
+    this.namePattern = namePattern;
+  }
+
+  public boolean isAny() {
+    return packagePattern.isAny() && namePattern.isAny();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java
new file mode 100644
index 0000000..078fab6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public class KeepTarget {
+
+  public static KeepTarget any() {
+    return KeepTarget.builder().setItem(KeepItemPattern.any()).build();
+  }
+
+  public static class Builder {
+
+    private KeepItemPattern item;
+    private KeepOptions options = KeepOptions.keepAll();
+
+    private Builder() {}
+
+    public Builder setItem(KeepItemPattern item) {
+      this.item = item;
+      return this;
+    }
+
+    public Builder setOptions(KeepOptions options) {
+      this.options = options;
+      return this;
+    }
+
+    public KeepTarget build() {
+      if (item == null) {
+        throw new KeepEdgeException("Target must define an item pattern");
+      }
+      return new KeepTarget(item, options);
+    }
+  }
+
+  private final KeepItemPattern item;
+  private final KeepOptions options;
+
+  private KeepTarget(KeepItemPattern item, KeepOptions options) {
+    this.item = item;
+    this.options = options;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java
new file mode 100644
index 0000000..50f94a6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java
@@ -0,0 +1,76 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public abstract class KeepUnqualfiedClassNamePattern {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static KeepUnqualfiedClassNamePattern any() {
+    return KeepClassNameAnyPattern.getInstance();
+  }
+
+  public static KeepUnqualfiedClassNamePattern exact(String className) {
+    return builder().exact(className).build();
+  }
+
+  public static class Builder {
+
+    private KeepUnqualfiedClassNamePattern pattern;
+
+    public Builder any() {
+      pattern = KeepClassNameAnyPattern.getInstance();
+      return this;
+    }
+
+    public Builder exact(String className) {
+      pattern = new KeepClassNameExactPattern(className);
+      return this;
+    }
+
+    public KeepUnqualfiedClassNamePattern build() {
+      if (pattern == null) {
+        throw new KeepEdgeException("Invalid class name pattern: null");
+      }
+      return pattern;
+    }
+  }
+
+  private static class KeepClassNameAnyPattern extends KeepUnqualfiedClassNamePattern {
+
+    private static KeepClassNameAnyPattern INSTANCE = null;
+
+    public static KeepClassNameAnyPattern getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new KeepClassNameAnyPattern();
+      }
+      return INSTANCE;
+    }
+
+    private KeepClassNameAnyPattern() {}
+
+    @Override
+    public boolean isAny() {
+      return true;
+    }
+  }
+
+  private static class KeepClassNameExactPattern extends KeepUnqualfiedClassNamePattern {
+
+    private final String className;
+
+    private KeepClassNameExactPattern(String className) {
+      this.className = className;
+    }
+
+    @Override
+    public boolean isAny() {
+      return false;
+    }
+  }
+
+  public abstract boolean isAny();
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUsageKind.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUsageKind.java
new file mode 100644
index 0000000..0e4a9e9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUsageKind.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+public abstract class KeepUsageKind {
+
+  /**
+   * Symbolically referenced classes, method and fields.
+   *
+   * <p>A symbolic reference of class, method or field is a reference that may not make use of the
+   * item for its "runtime" effect.
+   *
+   * <p>Symbolic class references could be class constants, checkcast, instanceof as well as the use
+   * of the class type in type hierarchies and annotations. The use of a class reference does not
+   * imply that the class is ever instantiated as an object instance.
+   *
+   * <p>For methods, a reference may be in use by the need to retain the method without it actually
+   * ever being called. This may be the case for methods with overrides where removal of a method
+   * could cause a semantic change to the program. Similar for fields.
+   */
+  public static KeepUsageKind symbolicReference() {
+    return SymbolicReference.getInstance();
+  }
+
+  /**
+   * Actual usages include instantiated classes, executed methods and accessed fields.
+   *
+   * <p>An actual use is stronger than "symbolic reference" and thus implies that the item is also
+   * "referenced". Thus, the set of all used items is a subset of the referenced items.
+   *
+   * <p>An actual use of class means that the class has instantiated instances. For methods, it
+   * means that the method is invoked and executed in the typical sense. For fields, it means that
+   * the field is read from or written to.
+   *
+   * <p>Note, it is possible for a static method or field to be used but its holder class still only
+   * being "referenced" as there are no instances of the class.
+   */
+  public static KeepUsageKind actualUse() {
+    return ActualUse.getInstance();
+  }
+
+  private static class SymbolicReference extends KeepUsageKind {
+    private static SymbolicReference INSTANCE = null;
+
+    public static SymbolicReference getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new SymbolicReference();
+      }
+      return INSTANCE;
+    }
+
+    private SymbolicReference() {}
+  }
+
+  private static class ActualUse extends KeepUsageKind {
+    private static ActualUse INSTANCE = null;
+
+    public static ActualUse getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new ActualUse();
+      }
+      return INSTANCE;
+    }
+
+    private ActualUse() {}
+  }
+
+  private KeepUsageKind() {}
+}
diff --git a/src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeApiTest.java b/src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeApiTest.java
new file mode 100644
index 0000000..f4a1b8d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeApiTest.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.experimental.keepanno.ast;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepEdgeApiTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public KeepEdgeApiTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  @Test
+  public void testKeepAll() {
+    // Equivalent of: -keep class * { *; }
+    KeepEdge edge =
+        KeepEdge.builder()
+            .setConsequences(
+                KeepConsequences.builder()
+                    .addTarget(KeepTarget.builder().setItem(KeepItemPattern.any()).build())
+                    .build())
+            .build();
+  }
+
+  @Test
+  public void testKeepClass() throws Exception {
+    // Equivalent of: -keep class com.example.Foo {}
+    KeepQualifiedClassNamePattern clazz = KeepQualifiedClassNamePattern.exact("com.example.Foo");
+    KeepItemPattern item = KeepItemPattern.builder().setClassPattern(clazz).build();
+    KeepTarget target = KeepTarget.builder().setItem(item).build();
+    KeepConsequences consequences = KeepConsequences.builder().addTarget(target).build();
+    KeepEdge edge = KeepEdge.builder().setConsequences(consequences).build();
+  }
+
+  @Test
+  public void testKeepInitIfReferenced() throws Exception {
+    // Equivalent of: -if class com.example.Foo -keep class com.example.Foo { void <init>(); }
+    KeepQualifiedClassNamePattern classPattern =
+        KeepQualifiedClassNamePattern.exact("com.example.Foo");
+    KeepEdge.builder()
+        .setPreconditions(
+            KeepPreconditions.builder()
+                .addCondition(
+                    KeepCondition.builder()
+                        .setUsageKind(KeepUsageKind.symbolicReference())
+                        .setItem(KeepItemPattern.builder().setClassPattern(classPattern).build())
+                        .build())
+                .build())
+        .setConsequences(
+            KeepConsequences.builder()
+                .addTarget(
+                    KeepTarget.builder()
+                        .setItem(
+                            KeepItemPattern.builder()
+                                .setClassPattern(classPattern)
+                                .setMembersPattern(
+                                    KeepMembersPattern.builder()
+                                        .addMethodPattern(
+                                            KeepMethodPattern.builder()
+                                                .setNamePattern(KeepMethodNamePattern.initializer())
+                                                .setParametersPattern(
+                                                    KeepMethodParametersPattern.none())
+                                                .setReturnTypeVoid()
+                                                .build())
+                                        .build())
+                                .build())
+                        .build())
+                .build())
+        .build();
+  }
+}