package org.checkerframework.common.accumulation;

import com.github.javaparser.ParseProblemException;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.expr.BinaryExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.UnaryExpr;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.util.Elements;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.accumulation.AccumulationChecker.AliasAnalysis;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.returnsreceiver.ReturnsReceiverAnnotatedTypeFactory;
import org.checkerframework.common.returnsreceiver.ReturnsReceiverChecker;
import org.checkerframework.common.returnsreceiver.qual.This;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.type.ElementQualifierHierarchy;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator;
import org.checkerframework.framework.type.treeannotator.TreeAnnotator;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypeSystemError;
import org.checkerframework.javacutil.UserError;
import org.plumelib.util.CollectionsPlume;

/**
 * An annotated type factory for an accumulation checker.
 *
 * <p>New accumulation checkers should extend this class and implement a constructor, which should
 * take a {@link BaseTypeChecker} and call both the constructor defined in this class and {@link
 * #postInit()}.
 */
public abstract class AccumulationAnnotatedTypeFactory
    extends GenericAnnotatedTypeFactory<
        AccumulationValue, AccumulationStore, AccumulationTransfer, AccumulationAnalysis> {

  /** The typechecker associated with this factory. */
  public final AccumulationChecker accumulationChecker;

  /**
   * The canonical top annotation for this accumulation checker: an instance of the accumulator
   * annotation with no arguments.
   */
  public final AnnotationMirror top;

  /** The canonical bottom annotation for this accumulation checker. */
  public final AnnotationMirror bottom;

  /**
   * The annotation that accumulates things in this accumulation checker. Must be an annotation with
   * exactly one field named "value" whose type is a String array.
   */
  private final Class<? extends Annotation> accumulator;

  /**
   * The predicate annotation for this accumulation analysis, or null if predicates are not
   * supported. A predicate annotation must have a single element named "value" of type String.
   */
  private final @MonotonicNonNull Class<? extends Annotation> predicate;

  /**
   * Create an annotated type factory for an accumulation checker.
   *
   * @param checker the checker
   * @param accumulator the accumulator type in the hierarchy. Must be an annotation with a single
   *     argument named "value" whose type is a String array.
   * @param bottom the bottom type in the hierarchy, which must be a subtype of {@code accumulator}.
   *     The bottom type should be an annotation with no arguments.
   * @param predicate the predicate annotation. Either null (if predicates are not supported), or an
   *     annotation with a single element named "value" whose type is a String.
   */
  protected AccumulationAnnotatedTypeFactory(
      BaseTypeChecker checker,
      Class<? extends Annotation> accumulator,
      Class<? extends Annotation> bottom,
      @Nullable Class<? extends Annotation> predicate) {
    super(checker);
    if (!(checker instanceof AccumulationChecker)) {
      throw new TypeSystemError(
          "AccumulationAnnotatedTypeFactory cannot be used with a checker "
              + "class that is not a subtype of AccumulationChecker. Found class: "
              + checker.getClass());
    }
    this.accumulationChecker = (AccumulationChecker) checker;

    this.accumulator = accumulator;
    // Check that the requirements of the accumulator are met.
    Method[] accDeclaredMethods = accumulator.getDeclaredMethods();
    if (accDeclaredMethods.length != 1) {
      rejectMalformedAccumulator("have exactly one element");
    }

    Method accValue = accDeclaredMethods[0];
    if (accValue.getName() != "value") { // interned
      rejectMalformedAccumulator("name its element \"value\"");
    }
    if (!accValue.getReturnType().isInstance(new String[0])) {
      rejectMalformedAccumulator("have an element of type String[]");
    }
    if (accValue.getDefaultValue() == null || ((String[]) accValue.getDefaultValue()).length != 0) {
      rejectMalformedAccumulator("have the empty String array {} as its default value");
    }

    this.predicate = predicate;
    // If there is a predicate annotation, check that its requirements are met.
    if (predicate != null) {
      Method[] predDeclaredMethods = predicate.getDeclaredMethods();
      if (predDeclaredMethods.length != 1) {
        rejectMalformedPredicate("have exactly one element");
      }
      Method predValue = predDeclaredMethods[0];
      if (predValue.getName() != "value") { // interned
        rejectMalformedPredicate("name its element \"value\"");
      }
      if (!predValue.getReturnType().isInstance("")) {
        rejectMalformedPredicate("have an element of type String");
      }
    }

    this.bottom = AnnotationBuilder.fromClass(elements, bottom);
    this.top = createAccumulatorAnnotation(Collections.emptyList());

    // Every subclass must call postInit!  This does not do so.
  }

  /**
   * Create an annotated type factory for an accumulation checker.
   *
   * @param checker the checker
   * @param accumulator the accumulator type in the hierarchy. Must be an annotation with a single
   *     argument named "value" whose type is a String array.
   * @param bottom the bottom type in the hierarchy, which must be a subtype of {@code accumulator}.
   *     The bottom type should be an annotation with no arguments.
   */
  protected AccumulationAnnotatedTypeFactory(
      BaseTypeChecker checker,
      Class<? extends Annotation> accumulator,
      Class<? extends Annotation> bottom) {
    this(checker, accumulator, bottom, null);
  }

  /**
   * Common error message for malformed accumulator annotation.
   *
   * @param missing what is missing from the accumulator, suitable for use in this string to replace
   *     $MISSING$: "The accumulator annotation Foo must $MISSING$."
   */
  private void rejectMalformedAccumulator(String missing) {
    rejectMalformedAnno("accumulator", accumulator, missing);
  }

  /**
   * Common error message for malformed predicate annotation.
   *
   * @param missing what is missing from the predicate, suitable for use in this string to replace
   *     $MISSING$: "The predicate annotation Foo must $MISSING$."
   */
  private void rejectMalformedPredicate(String missing) {
    rejectMalformedAnno("predicate", predicate, missing);
  }

  /**
   * Common error message implementation. Call rejectMalformedAccumulator or
   * rejectMalformedPredicate as appropriate, rather than this method directly.
   *
   * @param annoTypeName the display name for the type of malformed annotation, such as
   *     "accumulator"
   * @param anno the malformed annotation
   * @param missing what is missing from the annotation, suitable for use in this string to replace
   *     $MISSING$: "The accumulator annotation Foo must $MISSING$."
   */
  private void rejectMalformedAnno(
      String annoTypeName, Class<? extends Annotation> anno, String missing) {
    throw new BugInCF("The " + annoTypeName + " annotation " + anno + " must " + missing + ".");
  }

  /**
   * Creates a new instance of the accumulator annotation that contains the elements of {@code
   * values}.
   *
   * @param values the arguments to the annotation. The values can contain duplicates and can be in
   *     any order.
   * @return an annotation mirror representing the accumulator annotation with {@code values}'s
   *     arguments; this is top if {@code values} is empty
   */
  public AnnotationMirror createAccumulatorAnnotation(List<String> values) {
    AnnotationBuilder builder = new AnnotationBuilder(processingEnv, accumulator);
    builder.setValue("value", CollectionsPlume.withoutDuplicatesSorted(values));
    return builder.build();
  }

  /**
   * Creates a new instance of the accumulator annotation that contains exactly one value.
   *
   * @param value the argument to the annotation
   * @return an annotation mirror representing the accumulator annotation with {@code value} as its
   *     argument
   */
  public AnnotationMirror createAccumulatorAnnotation(String value) {
    AnnotationBuilder builder = new AnnotationBuilder(processingEnv, accumulator);
    builder.setValue("value", Collections.singletonList(value));
    return builder.build();
  }

  /**
   * Returns true if the return type of the given method invocation tree has an @This annotation
   * from the Returns Receiver Checker.
   *
   * @param tree a method invocation tree
   * @return true if the method being invoked returns its receiver
   */
  public boolean returnsThis(MethodInvocationTree tree) {
    if (!accumulationChecker.isEnabled(AliasAnalysis.RETURNS_RECEIVER)) {
      return false;
    }
    // Must call `getTypeFactoryOfSubchecker` each time, not store and reuse.
    ReturnsReceiverAnnotatedTypeFactory rrATF =
        getTypeFactoryOfSubchecker(ReturnsReceiverChecker.class);
    ExecutableElement methodEle = TreeUtils.elementFromUse(tree);
    AnnotatedExecutableType methodAtm = rrATF.getAnnotatedType(methodEle);
    AnnotatedTypeMirror rrType = methodAtm.getReturnType();
    return rrType != null && rrType.hasPrimaryAnnotation(This.class);
  }

  /**
   * Is the given annotation an accumulator annotation? Returns false if the argument is {@link
   * #bottom}.
   *
   * @param anm an annotation mirror
   * @return true if the annotation mirror is an instance of this factory's accumulator annotation
   */
  public boolean isAccumulatorAnnotation(AnnotationMirror anm) {
    return areSameByClass(anm, accumulator);
  }

  @Override
  protected TreeAnnotator createTreeAnnotator() {
    return new ListTreeAnnotator(super.createTreeAnnotator(), new AccumulationTreeAnnotator(this));
  }

  /**
   * This tree annotator implements the following rule(s):
   *
   * <dl>
   *   <dt>RRA
   *   <dd>If a method returns its receiver, and the receiver has an accumulation type, then the
   *       default type of the method's return value is the type of the receiver.
   * </dl>
   */
  protected class AccumulationTreeAnnotator extends TreeAnnotator {

    /**
     * Creates an instance of this tree annotator for the given type factory.
     *
     * @param factory the type factory
     */
    public AccumulationTreeAnnotator(AccumulationAnnotatedTypeFactory factory) {
      super(factory);
    }

    /**
     * Implements rule RRA.
     *
     * <p>This implementation propagates types from the receiver to the return value, without
     * change. Subclasses may override this method to also add additional properties, as
     * appropriate.
     *
     * @param tree a method invocation tree
     * @param type the type of {@code tree} (i.e. the return type of the invoked method). Is
     *     (possibly) side-effected by this method.
     * @return nothing, works by side-effect on {@code type}
     */
    @Override
    public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror type) {
      if (returnsThis(tree)) {
        // There is a @This annotation on the return type of the invoked method.
        ExpressionTree receiverTree = TreeUtils.getReceiverTree(tree.getMethodSelect());
        AnnotationMirror returnAnno = type.getPrimaryAnnotationInHierarchy(top);
        AnnotationMirror glbAnno;
        if (receiverTree == null) {
          glbAnno = returnAnno;
        } else {
          AnnotatedTypeMirror receiverType = getAnnotatedType(receiverTree);
          // The current type of the receiver, or top if none exists.
          AnnotationMirror receiverAnno = receiverType.getPrimaryAnnotationInHierarchy(top);
          glbAnno =
              qualHierarchy.greatestLowerBoundShallow(
                  returnAnno,
                  type.getUnderlyingType(),
                  receiverAnno,
                  receiverType.getUnderlyingType());
        }

        type.replaceAnnotation(glbAnno);
      }
      return super.visitMethodInvocation(tree, type);
    }
  }

  @Override
  protected QualifierHierarchy createQualifierHierarchy() {
    return new AccumulationQualifierHierarchy(this.getSupportedTypeQualifiers(), this.elements);
  }

  /**
   * Returns all the values that anno has accumulated.
   *
   * @param anno an accumulator annotation; must not be bottom
   * @return the list of values the annotation has accumulated; it is a new list, so it is safe for
   *     clients to side-effect
   */
  public List<String> getAccumulatedValues(AnnotationMirror anno) {
    if (!isAccumulatorAnnotation(anno)) {
      throw new BugInCF(anno + " isn't an accumulator annotation");
    }
    List<String> values =
        AnnotationUtils.getElementValueArrayOrNull(anno, "value", String.class, false);
    if (values == null) {
      return Collections.emptyList();
    } else {
      return values;
    }
  }

  /**
   * Returns the accumulated values on the given (expression, usually) tree. This differs from
   * calling {@link #getAnnotatedType(Tree)}, because this version takes into account accumulated
   * methods that are stored on the value. This is useful when dealing with accumulated facts on
   * variables whose types are type variables (because type variable types cannot be refined
   * directly, due to the quirks of subtyping between type variables and its interactions with the
   * qualified type system).
   *
   * <p>The returned collection may be either a list or a set.
   *
   * @param tree a tree
   * @return the accumulated values for the given tree, including those stored on the value
   */
  public Collection<String> getAccumulatedValues(Tree tree) {
    AnnotatedTypeMirror type = getAnnotatedType(tree);
    AnnotationMirror anno = type.getPrimaryAnnotationInHierarchy(top);
    if (anno != null && isAccumulatorAnnotation(anno)) {
      return getAccumulatedValues(anno);
    } else if (anno == null) {
      // Handle type variables and wildcards.
      AccumulationValue inferredValue = getInferredValueFor(tree);
      if (inferredValue != null) {
        Set<String> accumulatedValues = inferredValue.getAccumulatedValues();
        if (accumulatedValues != null) {
          return accumulatedValues;
        }
      }
    }
    return Collections.emptyList();
  }

  /**
   * All accumulation analyses share a similar type hierarchy. This class implements the subtyping,
   * LUB, and GLB for that hierarchy. The lattice looks like:
   *
   * <pre>
   *       acc()
   *      /   \
   * acc(x)   acc(y) ...
   *      \   /
   *     acc(x,y) ...
   *        |
   *      bottom
   * </pre>
   *
   * Predicate subtyping is defined as follows:
   *
   * <ul>
   *   <li>An accumulator is a subtype of a predicate if substitution from the accumulator to the
   *       predicate makes the predicate true. For example, {@code Acc(A)} is a subtype of {@code
   *       AccPred("A || B")}, because when A is replaced with {@code true} and B is replaced with
   *       {@code false}, the resulting boolean formula evaluates to true.
   *   <li>A predicate P is a subtype of an accumulator iff after converting the accumulator into a
   *       predicate representing the conjunction of its elements, P is a subtype of that predicate
   *       according to the rule for subtyping between two predicates defined below.
   *   <li>A predicate P is a subtype of another predicate Q iff P and Q are equal. An extension
   *       point ({@link #isPredicateSubtype(String, String)}) is provided to allow more complex
   *       subtyping behavior between predicates. (The "correct" subtyping rule is that P is a
   *       subtype of Q iff P implies Q. That rule would require an SMT solver in the general case,
   *       which is undesirable because it would require an external dependency. A user can override
   *       {@link #isPredicateSubtype(String, String)} if they require more precise subtyping; the
   *       check described here is overly conservative (and therefore sound), but not very precise.)
   * </ul>
   */
  protected class AccumulationQualifierHierarchy extends ElementQualifierHierarchy {

    /**
     * Creates a ElementQualifierHierarchy from the given classes.
     *
     * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy
     * @param elements element utils
     */
    protected AccumulationQualifierHierarchy(
        Collection<Class<? extends Annotation>> qualifierClasses, Elements elements) {
      super(qualifierClasses, elements, AccumulationAnnotatedTypeFactory.this);
    }

    /**
     * GLB in this type system is set union of the arguments of the two annotations, unless one of
     * them is bottom, in which case the result is also bottom.
     */
    @Override
    public AnnotationMirror greatestLowerBoundQualifiers(AnnotationMirror a1, AnnotationMirror a2) {
      if (AnnotationUtils.areSame(a1, bottom) || AnnotationUtils.areSame(a2, bottom)) {
        return bottom;
      }

      if (isPolymorphicQualifier(a1) && isPolymorphicQualifier(a2)) {
        return a1;
      } else if (isPolymorphicQualifier(a1) || isPolymorphicQualifier(a2)) {
        return bottom;
      }

      // If either is a predicate, then both should be converted to predicates and and-ed.
      if (isPredicate(a1) || isPredicate(a2)) {
        String a1Pred = convertToPredicate(a1);
        String a2Pred = convertToPredicate(a2);
        // check for top
        if (a1Pred.isEmpty()) {
          return a2;
        } else if (a2Pred.isEmpty()) {
          return a1;
        } else {
          return createPredicateAnnotation("(" + a1Pred + ") && (" + a2Pred + ")");
        }
      }

      List<String> a1Val = getAccumulatedValues(a1);
      List<String> a2Val = getAccumulatedValues(a2);
      // Avoid creating new annotation objects in the common case.
      if (a1Val.containsAll(a2Val)) {
        return a1;
      }
      if (a2Val.containsAll(a1Val)) {
        return a2;
      }
      a1Val.addAll(a2Val); // union
      return createAccumulatorAnnotation(a1Val);
    }

    /**
     * LUB in this type system is set intersection of the arguments of the two annotations, unless
     * one of them is bottom, in which case the result is the other annotation.
     */
    @Override
    public AnnotationMirror leastUpperBoundQualifiers(AnnotationMirror a1, AnnotationMirror a2) {
      if (AnnotationUtils.areSame(a1, bottom)) {
        return a2;
      } else if (AnnotationUtils.areSame(a2, bottom)) {
        return a1;
      }

      if (isPolymorphicQualifier(a1) && isPolymorphicQualifier(a2)) {
        return a1;
      } else if (isPolymorphicQualifier(a1) || isPolymorphicQualifier(a2)) {
        return top;
      }

      // If either is a predicate, then both should be converted to predicates and or-ed.
      if (isPredicate(a1) || isPredicate(a2)) {
        String a1Pred = convertToPredicate(a1);
        String a2Pred = convertToPredicate(a2);
        // check for top
        if (a1Pred.isEmpty()) {
          return a1;
        } else if (a2Pred.isEmpty()) {
          return a2;
        } else {
          return createPredicateAnnotation("(" + a1Pred + ") || (" + a2Pred + ")");
        }
      }

      List<String> a1Val = getAccumulatedValues(a1);
      List<String> a2Val = getAccumulatedValues(a2);
      // Avoid creating new annotation objects in the common case.
      if (a1Val.containsAll(a2Val)) {
        return a2;
      }
      if (a2Val.containsAll(a1Val)) {
        return a1;
      }
      a1Val.retainAll(a2Val); // intersection
      return createAccumulatorAnnotation(a1Val);
    }

    /**
     * {@inheritDoc}
     *
     * <p>isSubtype in this type system is subset.
     */
    @Override
    public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) {
      if (AnnotationUtils.areSame(subAnno, bottom)) {
        return true;
      } else if (AnnotationUtils.areSame(superAnno, bottom)) {
        return false;
      }

      if (isPolymorphicQualifier(subAnno)) {
        if (isPolymorphicQualifier(superAnno)) {
          return true;
        } else {
          // Use this slightly more expensive conversion here because this is a rare code
          // path and it's simpler to read than checking for both predicate and
          // non-predicate forms of top.
          return "".equals(convertToPredicate(superAnno));
        }
      } else if (isPolymorphicQualifier(superAnno)) {
        // Polymorphic annotations are only a supertype of other polymorphic annotations and
        // the bottom type, both of which have already been checked above.
        return false;
      }

      if (isPredicate(subAnno)) {
        return isPredicateSubtype(convertToPredicate(subAnno), convertToPredicate(superAnno));
      } else if (isPredicate(superAnno)) {
        return evaluatePredicate(subAnno, convertToPredicate(superAnno));
      }

      List<String> subVal = getAccumulatedValues(subAnno);
      List<String> superVal = getAccumulatedValues(superAnno);
      return subVal.containsAll(superVal);
    }
  }

  /**
   * Extension point for subtyping behavior between predicates. This implementation conservatively
   * returns true only if the predicates are equal, or if the prospective supertype (q) is
   * equivalent to top (that is, the empty string).
   *
   * @param p a predicate
   * @param q another predicate
   * @return true if p is a subtype of q
   */
  protected boolean isPredicateSubtype(String p, String q) {
    return "".equals(q) || p.equals(q);
  }

  /**
   * Evaluates whether the accumulator annotation {@code subAnno} makes the predicate {@code pred}
   * true.
   *
   * @param subAnno an accumulator annotation
   * @param pred a predicate
   * @return whether the accumulator annotation satisfies the predicate
   */
  protected boolean evaluatePredicate(AnnotationMirror subAnno, String pred) {
    if (!isAccumulatorAnnotation(subAnno)) {
      throw new BugInCF(
          "tried to evaluate a predicate using an annotation that wasn't an accumulator: "
              + subAnno);
    }
    List<String> trueVariables = getAccumulatedValues(subAnno);
    return evaluatePredicate(trueVariables, pred);
  }

  /**
   * Checks that the given annotation either:
   *
   * <ul>
   *   <li>does not contain a predicate, or
   *   <li>contains a parse-able predicate
   * </ul>
   *
   * Used by the visitor to throw "predicate" errors; thus must be package-private.
   *
   * @param anm any annotation supported by this checker
   * @return null if there is nothing wrong with the annotation, or an error message indicating the
   *     problem if it has an invalid predicate
   */
  /*package-private*/ @Nullable String isValidPredicate(AnnotationMirror anm) {
    String pred = convertToPredicate(anm);
    try {
      evaluatePredicate(Collections.emptyList(), pred);
    } catch (UserError ue) {
      return ue.getLocalizedMessage();
    }
    return null;
  }

  /**
   * Evaluates whether treating the variables in {@code trueVariables} as {@code true} literals (and
   * all other names as {@code false} literals) makes the predicate {@code pred} evaluate to true.
   *
   * @param trueVariables a list of names that should be replaced with {@code true}
   * @param pred a predicate
   * @return whether the true variables satisfy the predicate
   */
  protected boolean evaluatePredicate(List<String> trueVariables, String pred) {
    Expression expression;
    try {
      expression = StaticJavaParser.parseExpression(pred);
    } catch (ParseProblemException p) {
      throw new UserError("unparsable predicate: " + pred + ". Parse exception: " + p);
    }
    return evaluateBooleanExpression(expression, trueVariables);
  }

  /**
   * Evaluates a boolean expression, in JavaParser format, that contains only and, or, parentheses,
   * logical complement, and boolean literal nodes.
   *
   * @param expression a JavaParser boolean expression
   * @param trueVariables the names of the variables that should be considered "true"; all other
   *     literals are considered "false"
   * @return the result of evaluating the expression
   */
  private boolean evaluateBooleanExpression(Expression expression, List<String> trueVariables) {
    if (expression.isNameExpr()) {
      return trueVariables.contains(expression.asNameExpr().getNameAsString());
    } else if (expression.isBinaryExpr()) {
      if (expression.asBinaryExpr().getOperator() == BinaryExpr.Operator.OR) {
        return evaluateBooleanExpression(expression.asBinaryExpr().getLeft(), trueVariables)
            || evaluateBooleanExpression(expression.asBinaryExpr().getRight(), trueVariables);
      } else if (expression.asBinaryExpr().getOperator() == BinaryExpr.Operator.AND) {
        return evaluateBooleanExpression(expression.asBinaryExpr().getLeft(), trueVariables)
            && evaluateBooleanExpression(expression.asBinaryExpr().getRight(), trueVariables);
      }
    } else if (expression.isEnclosedExpr()) {
      return evaluateBooleanExpression(expression.asEnclosedExpr().getInner(), trueVariables);
    } else if (expression.isUnaryExpr()) {
      if (expression.asUnaryExpr().getOperator() == UnaryExpr.Operator.LOGICAL_COMPLEMENT) {
        return !evaluateBooleanExpression(expression.asUnaryExpr().getExpression(), trueVariables);
      }
    }
    // This could be a BugInCF if there is a bug in the code above.
    throw new UserError(
        "encountered an unexpected type of expression in a "
            + "predicate expression: "
            + expression
            + " was of type "
            + expression.getClass());
  }

  /**
   * Creates a new predicate annotation from the given string.
   *
   * @param p a valid predicate
   * @return an annotation representing that predicate
   */
  protected AnnotationMirror createPredicateAnnotation(String p) {
    AnnotationBuilder builder = new AnnotationBuilder(processingEnv, predicate);
    builder.setValue("value", p);
    return builder.build();
  }

  /**
   * Converts the given annotation mirror to a predicate.
   *
   * @param anno an annotation
   * @return the predicate, as a String, that is equivalent to that annotation. May return the empty
   *     string.
   */
  protected String convertToPredicate(AnnotationMirror anno) {
    if (AnnotationUtils.areSame(anno, bottom)) {
      return "false";
    } else if (isPredicate(anno)) {
      String result = AnnotationUtils.getElementValueOrNull(anno, "value", String.class, false);
      return result == null ? "" : result;
    } else if (isAccumulatorAnnotation(anno)) {
      List<String> values = getAccumulatedValues(anno);
      StringJoiner sj = new StringJoiner(" && ");
      for (String value : values) {
        sj.add(value);
      }
      return sj.toString();
    } else {
      throw new BugInCF("annotation is not bottom, a predicate, or an accumulator: " + anno);
    }
  }

  /**
   * Returns true if anno is a predicate annotation.
   *
   * @param anno an annotation
   * @return true if anno is a predicate annotation
   */
  protected boolean isPredicate(AnnotationMirror anno) {
    return predicate != null && areSameByClass(anno, predicate);
  }
}
