Support repeatable annotations

Also consolidate handling of annotation meta-information (target,
retention, and now repeatable).

MOE_MIGRATED_REVID=138010789
diff --git a/java/com/google/turbine/binder/CanonicalTypeBinder.java b/java/com/google/turbine/binder/CanonicalTypeBinder.java
index edfc02d..0e4e267 100644
--- a/java/com/google/turbine/binder/CanonicalTypeBinder.java
+++ b/java/com/google/turbine/binder/CanonicalTypeBinder.java
@@ -67,8 +67,7 @@
         base.enclosingScope(),
         base.scope(),
         base.memberImports(),
-        base.annotationRetention(),
-        base.annotationTarget(),
+        base.annotationMetadata(),
         base.annotations(),
         base.source());
   }
diff --git a/java/com/google/turbine/binder/ConstBinder.java b/java/com/google/turbine/binder/ConstBinder.java
index 959e62a..03c728e 100644
--- a/java/com/google/turbine/binder/ConstBinder.java
+++ b/java/com/google/turbine/binder/ConstBinder.java
@@ -19,6 +19,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.turbine.binder.bound.AnnotationMetadata;
+import com.google.turbine.binder.bound.ClassValue;
 import com.google.turbine.binder.bound.EnumConstantValue;
 import com.google.turbine.binder.bound.SourceTypeBoundClass;
 import com.google.turbine.binder.bound.TypeBoundClass;
@@ -43,6 +45,7 @@
 import com.google.turbine.type.Type.ArrayTy;
 import com.google.turbine.type.Type.ClassTy;
 import com.google.turbine.type.Type.ClassTy.SimpleClassTy;
+import com.google.turbine.type.Type.TyKind;
 import com.google.turbine.type.Type.TyVar;
 import com.google.turbine.type.Type.WildLowerBoundedTy;
 import com.google.turbine.type.Type.WildTy;
@@ -50,9 +53,7 @@
 import com.google.turbine.type.Type.WildUpperBoundedTy;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.RetentionPolicy;
-import java.util.EnumSet;
 import java.util.Map;
-import javax.annotation.Nullable;
 
 /** Binding pass to evaluate constant expressions. */
 public class ConstBinder {
@@ -93,8 +94,7 @@
         base.enclosingScope(),
         base.scope(),
         base.memberImports(),
-        bindRetention(base.kind(), annos),
-        bindTarget(base.kind(), annos),
+        bindAnnotationMetadata(base.kind(), annos),
         annos,
         base.source());
   }
@@ -143,46 +143,42 @@
     return new ParamInfo(bindType(base.type()), annos, base.synthetic());
   }
 
-  /** Returns the {@link RetentionPolicy} for an annotation declaration, or {@code null}. */
-  @Nullable
-  static RetentionPolicy bindRetention(TurbineTyKind kind, Iterable<AnnoInfo> annotations) {
+  static AnnotationMetadata bindAnnotationMetadata(
+      TurbineTyKind kind, Iterable<AnnoInfo> annotations) {
     if (kind != TurbineTyKind.ANNOTATION) {
       return null;
     }
-    for (AnnoInfo annotation : annotations) {
-      if (annotation.sym().toString().equals("java/lang/annotation/Retention")) {
-        Const value = annotation.values().get("value");
-        if (value.kind() != Const.Kind.ENUM_CONSTANT) {
-          break;
-        }
-        EnumConstantValue enumValue = (EnumConstantValue) value;
-        if (!enumValue.sym().owner().toString().equals("java/lang/annotation/RetentionPolicy")) {
-          break;
-        }
-        return RetentionPolicy.valueOf(enumValue.sym().name());
-      }
-    }
-    return RetentionPolicy.CLASS;
-  }
-
-  /** Returns the target {@link ElementType}s for an annotation declaration, or {@code null}. */
-  @Nullable
-  static ImmutableSet<ElementType> bindTarget(TurbineTyKind kind, Iterable<AnnoInfo> annotations) {
-    if (kind != TurbineTyKind.ANNOTATION) {
-      return null;
-    }
+    RetentionPolicy retention = null;
+    ImmutableSet<ElementType> target = null;
+    ClassSymbol repeatable = null;
     for (AnnoInfo annotation : annotations) {
       switch (annotation.sym().binaryName()) {
+        case "java/lang/annotation/Retention":
+          retention = bindRetention(annotation);
+          break;
         case "java/lang/annotation/Target":
-          return bindTarget(annotation);
+          target = bindTarget(annotation);
+          break;
+        case "java/lang/annotation/Repeatable":
+          repeatable = bindRepeatable(annotation);
+          break;
         default:
           break;
       }
     }
-    EnumSet<ElementType> target = EnumSet.allOf(ElementType.class);
-    target.remove(ElementType.TYPE_USE);
-    target.remove(ElementType.TYPE_PARAMETER);
-    return ImmutableSet.copyOf(target);
+    return new AnnotationMetadata(retention, target, repeatable);
+  }
+
+  private static RetentionPolicy bindRetention(AnnoInfo annotation) {
+    Const value = annotation.values().get("value");
+    if (value.kind() != Kind.ENUM_CONSTANT) {
+      return null;
+    }
+    EnumConstantValue enumValue = (EnumConstantValue) value;
+    if (!enumValue.sym().owner().toString().equals("java/lang/annotation/RetentionPolicy")) {
+      return null;
+    }
+    return RetentionPolicy.valueOf(enumValue.sym().name());
   }
 
   private static ImmutableSet<ElementType> bindTarget(AnnoInfo annotation) {
@@ -205,6 +201,18 @@
     return result.build();
   }
 
+  private static ClassSymbol bindRepeatable(AnnoInfo annotation) {
+    Const value = annotation.values().get("value");
+    if (value.kind() != Kind.CLASS_LITERAL) {
+      return null;
+    }
+    Type type = ((ClassValue) value).type();
+    if (type.tyKind() != TyKind.CLASS_TY) {
+      return null;
+    }
+    return ((ClassTy) type).sym();
+  }
+
   private static void bindTargetElement(
       ImmutableSet.Builder<ElementType> target, EnumConstantValue enumVal) {
     if (enumVal.sym().owner().binaryName().equals("java/lang/annotation/ElementType")) {
diff --git a/java/com/google/turbine/binder/DisambiguateTypeAnnotations.java b/java/com/google/turbine/binder/DisambiguateTypeAnnotations.java
index 656d511..0f6c9b4 100644
--- a/java/com/google/turbine/binder/DisambiguateTypeAnnotations.java
+++ b/java/com/google/turbine/binder/DisambiguateTypeAnnotations.java
@@ -16,8 +16,14 @@
 
 package com.google.turbine.binder;
 
+import static com.google.common.collect.Iterables.getOnlyElement;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.turbine.binder.bound.AnnotationValue;
 import com.google.turbine.binder.bound.SourceTypeBoundClass;
 import com.google.turbine.binder.bound.TypeBoundClass;
 import com.google.turbine.binder.bound.TypeBoundClass.FieldInfo;
@@ -25,6 +31,7 @@
 import com.google.turbine.binder.bound.TypeBoundClass.ParamInfo;
 import com.google.turbine.binder.env.Env;
 import com.google.turbine.binder.sym.ClassSymbol;
+import com.google.turbine.model.Const;
 import com.google.turbine.type.AnnoInfo;
 import com.google.turbine.type.Type;
 import com.google.turbine.type.Type.ArrayTy;
@@ -33,6 +40,8 @@
 import com.google.turbine.type.Type.PrimTy;
 import com.google.turbine.type.Type.TyVar;
 import java.lang.annotation.ElementType;
+import java.util.Collection;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -73,9 +82,8 @@
         base.enclosingScope(),
         base.scope(),
         base.memberImports(),
-        base.annotationRetention(),
-        base.annotationTarget(),
-        base.annotations(),
+        base.annotationMetadata(),
+        groupRepeated(env, base.annotations()),
         base.source());
   }
 
@@ -133,9 +141,12 @@
       Type type,
       ImmutableList<AnnoInfo> annotations,
       Builder<AnnoInfo> declarationAnnotations) {
+    // desugar @Repeatable annotations before disambiguating: annotation containers may target
+    // a subset of the types targeted by their element annotation
+    annotations = groupRepeated(env, annotations);
     ImmutableList.Builder<AnnoInfo> typeAnnotations = ImmutableList.builder();
     for (AnnoInfo anno : annotations) {
-      Set<ElementType> target = env.get(anno.sym()).annotationTarget();
+      Set<ElementType> target = env.get(anno.sym()).annotationMetadata().target();
       if (target.contains(ElementType.TYPE_USE)) {
         typeAnnotations.add(anno);
       }
@@ -218,7 +229,7 @@
       Builder<AnnoInfo> removed) {
     ImmutableList.Builder<AnnoInfo> result = ImmutableList.builder();
     for (AnnoInfo anno : annos) {
-      Set<ElementType> target = env.get(anno.sym()).annotationTarget();
+      Set<ElementType> target = env.get(anno.sym()).annotationMetadata().target();
       if (target.contains(ElementType.TYPE_USE)) {
         result.add(anno);
       } else {
@@ -228,4 +239,44 @@
     result.addAll(extra);
     return result.build();
   }
+
+  /**
+   * Group repeated annotations and wrap them in their container annotation.
+   *
+   * <p>For example, convert {@code @Foo @Foo} to {@code @Foos({@Foo, @Foo})}.
+   *
+   * <p>This method is used by {@link DisambiguateTypeAnnotations} for declaration annotations, and
+   * by {@link Lower} for type annotations. We could group type annotations here, but it would
+   * require another rewrite pass.
+   */
+  public static ImmutableList<AnnoInfo> groupRepeated(
+      Env<ClassSymbol, TypeBoundClass> env, ImmutableList<AnnoInfo> annotations) {
+    Multimap<ClassSymbol, AnnoInfo> repeated = LinkedHashMultimap.create();
+    for (AnnoInfo anno : annotations) {
+      repeated.put(anno.sym(), anno);
+    }
+    Builder<AnnoInfo> result = ImmutableList.builder();
+    for (Map.Entry<ClassSymbol, Collection<AnnoInfo>> entry : repeated.asMap().entrySet()) {
+      ClassSymbol symbol = entry.getKey();
+      Collection<AnnoInfo> infos = entry.getValue();
+      if (infos.size() > 1) {
+        Builder<Const> elements = ImmutableList.builder();
+        for (AnnoInfo element : infos) {
+          elements.add(new AnnotationValue(element.sym(), element.values()));
+        }
+        ClassSymbol container = env.get(symbol).annotationMetadata().repeatable();
+        if (container == null) {
+          throw new AssertionError(symbol);
+        }
+        result.add(
+            new AnnoInfo(
+                container,
+                null,
+                ImmutableMap.of("value", new Const.ArrayInitValue(elements.build()))));
+      } else {
+        result.add(getOnlyElement(infos));
+      }
+    }
+    return result.build();
+  }
 }
diff --git a/java/com/google/turbine/binder/TypeBinder.java b/java/com/google/turbine/binder/TypeBinder.java
index 21920ef..a1feb37 100644
--- a/java/com/google/turbine/binder/TypeBinder.java
+++ b/java/com/google/turbine/binder/TypeBinder.java
@@ -19,7 +19,6 @@
 import com.google.common.base.Verify;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.turbine.binder.bound.HeaderBoundClass;
 import com.google.turbine.binder.bound.SourceHeaderBoundClass;
 import com.google.turbine.binder.bound.SourceTypeBoundClass;
@@ -229,8 +228,7 @@
         enclosingScope,
         scope,
         base.memberImports(),
-        /*retention*/ null,
-        /*target*/ ImmutableSet.of(),
+        /* annotation metadata */ null,
         annotations,
         base.source());
   }
diff --git a/java/com/google/turbine/binder/bound/AnnotationMetadata.java b/java/com/google/turbine/binder/bound/AnnotationMetadata.java
new file mode 100644
index 0000000..1155e51
--- /dev/null
+++ b/java/com/google/turbine/binder/bound/AnnotationMetadata.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.turbine.binder.bound;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.turbine.binder.sym.ClassSymbol;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.RetentionPolicy;
+import java.util.EnumSet;
+
+/**
+ * Annotation metadata, e.g. from {@link @java.lang.annotation.Target}, {@link
+ * java.lang.annotation.Retention}, and {@link java.lang.annotation.Repeatable}.
+ */
+public class AnnotationMetadata {
+
+  private static final ImmutableSet<ElementType> DEFAULT_TARGETS = getDefaultElements();
+
+  private static ImmutableSet<ElementType> getDefaultElements() {
+    EnumSet<ElementType> values = EnumSet.allOf(ElementType.class);
+    values.remove(ElementType.TYPE_PARAMETER);
+    values.remove(ElementType.TYPE_USE);
+    return ImmutableSet.copyOf(values);
+  }
+
+  private final RetentionPolicy retention;
+  private final ImmutableSet<ElementType> target;
+  private final ClassSymbol repeatable;
+
+  public AnnotationMetadata(
+      RetentionPolicy retention,
+      ImmutableSet<ElementType> annotationTarget,
+      ClassSymbol repeatable) {
+    this.retention = firstNonNull(retention, RetentionPolicy.CLASS);
+    this.target = firstNonNull(annotationTarget, DEFAULT_TARGETS);
+    this.repeatable = repeatable;
+  }
+
+  /** The retention policy specified by the {@code @Retention} meta-annotation. */
+  public RetentionPolicy retention() {
+    return retention;
+  }
+
+  /** Target element types specified by the {@code @Target} meta-annotation. */
+  public ImmutableSet<ElementType> target() {
+    return target;
+  }
+
+  /** The container annotation for {@code @Repeated} annotations. */
+  public ClassSymbol repeatable() {
+    return repeatable;
+  }
+}
diff --git a/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java b/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
index 75008c4..fb2a3c5 100644
--- a/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
+++ b/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
@@ -18,7 +18,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.turbine.binder.lookup.CompoundScope;
 import com.google.turbine.binder.lookup.MemberImportIndex;
 import com.google.turbine.binder.sym.ClassSymbol;
@@ -28,8 +27,6 @@
 import com.google.turbine.type.AnnoInfo;
 import com.google.turbine.type.Type;
 import com.google.turbine.type.Type.ClassTy;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.RetentionPolicy;
 import javax.annotation.Nullable;
 
 /** A HeaderBoundClass for classes compiled from source. */
@@ -52,8 +49,7 @@
   private final CompoundScope enclosingScope;
   private final CompoundScope scope;
   private final MemberImportIndex memberImports;
-  private final RetentionPolicy retention;
-  private final ImmutableSet<ElementType> annotationTarget;
+  private final AnnotationMetadata annotationMetadata;
   private final ImmutableList<AnnoInfo> annotations;
   private final SourceFile source;
 
@@ -73,8 +69,7 @@
       CompoundScope enclosingScope,
       CompoundScope scope,
       MemberImportIndex memberImports,
-      RetentionPolicy retention,
-      ImmutableSet<ElementType> annotationTarget,
+      AnnotationMetadata annotationMetadata,
       ImmutableList<AnnoInfo> annotations,
       SourceFile source) {
     this.interfaceTypes = interfaceTypes;
@@ -92,8 +87,7 @@
     this.enclosingScope = enclosingScope;
     this.scope = scope;
     this.memberImports = memberImports;
-    this.retention = retention;
-    this.annotationTarget = annotationTarget;
+    this.annotationMetadata = annotationMetadata;
     this.annotations = annotations;
     this.source = source;
   }
@@ -152,13 +146,8 @@
   }
 
   @Override
-  public RetentionPolicy annotationRetention() {
-    return retention;
-  }
-
-  @Override
-  public ImmutableSet<ElementType> annotationTarget() {
-    return annotationTarget;
+  public AnnotationMetadata annotationMetadata() {
+    return annotationMetadata;
   }
 
   /** Declared fields. */
diff --git a/java/com/google/turbine/binder/bound/TypeBoundClass.java b/java/com/google/turbine/binder/bound/TypeBoundClass.java
index 6232b93..d942cbe 100644
--- a/java/com/google/turbine/binder/bound/TypeBoundClass.java
+++ b/java/com/google/turbine/binder/bound/TypeBoundClass.java
@@ -18,7 +18,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.turbine.binder.sym.FieldSymbol;
 import com.google.turbine.binder.sym.MethodSymbol;
 import com.google.turbine.binder.sym.TyVarSymbol;
@@ -27,8 +26,6 @@
 import com.google.turbine.tree.Tree.MethDecl;
 import com.google.turbine.type.AnnoInfo;
 import com.google.turbine.type.Type;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.RetentionPolicy;
 
 /** A bound node that augments {@link HeaderBoundClass} with type information. */
 public interface TypeBoundClass extends HeaderBoundClass {
@@ -44,11 +41,11 @@
   /** Declared methods. */
   ImmutableList<MethodInfo> methods();
 
-  /** Retention policy for annotation declarations, {@code null} for other declarations. */
-  RetentionPolicy annotationRetention();
-
-  /** Target element types for annotation declarations, {@code null} for other declarations. */
-  ImmutableSet<ElementType> annotationTarget();
+  /**
+   * Annotation metadata, e.g. from {@link @java.lang.annotation.Target}, {@link
+   * java.lang.annotation.Retention}, and {@link java.lang.annotation.Repeatable}.
+   */
+  AnnotationMetadata annotationMetadata();
 
   /** A type parameter declaration. */
   class TyVarInfo {
diff --git a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
index f0eed37..16aabc5 100644
--- a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
+++ b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.turbine.binder.bound.AnnotationMetadata;
 import com.google.turbine.binder.bound.BoundClass;
 import com.google.turbine.binder.bound.HeaderBoundClass;
 import com.google.turbine.binder.bound.TypeBoundClass;
@@ -36,6 +37,7 @@
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.ArrayValue;
+import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.ConstClassValue;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.EnumConstValue;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.Kind;
 import com.google.turbine.bytecode.ClassReader;
@@ -49,7 +51,6 @@
 import com.google.turbine.type.Type.ClassTy;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.RetentionPolicy;
-import java.util.EnumSet;
 import java.util.Map;
 import java.util.function.Function;
 import javax.annotation.Nullable;
@@ -389,60 +390,48 @@
     return methods.get();
   }
 
-  final Supplier<RetentionPolicy> retention =
+  final Supplier<AnnotationMetadata> annotationMetadata =
       Suppliers.memoize(
-          new Supplier<RetentionPolicy>() {
+          new Supplier<AnnotationMetadata>() {
             @Override
-            public RetentionPolicy get() {
+            public AnnotationMetadata get() {
               if ((access() & TurbineFlag.ACC_ANNOTATION) != TurbineFlag.ACC_ANNOTATION) {
                 return null;
               }
-              for (ClassFile.AnnotationInfo annotation : classFile.get().annotations()) {
-                if (!annotation.typeName().equals("Ljava/lang/annotation/Retention;")) {
-                  continue;
-                }
-                ElementValue val = annotation.elementValuePairs().get("value");
-                if (val.kind() != ElementValue.Kind.ENUM) {
-                  continue;
-                }
-                ElementValue.EnumConstValue enumVal = (ElementValue.EnumConstValue) val;
-                if (!enumVal.typeName().equals("Ljava/lang/annotation/RetentionPolicy;")) {
-                  continue;
-                }
-                return RetentionPolicy.valueOf(enumVal.constName());
-              }
-              return RetentionPolicy.CLASS;
-            }
-          });
-
-  @Override
-  public RetentionPolicy annotationRetention() {
-    return retention.get();
-  }
-
-  final Supplier<ImmutableSet<ElementType>> target =
-      Suppliers.memoize(
-          new Supplier<ImmutableSet<ElementType>>() {
-            @Override
-            public ImmutableSet<ElementType> get() {
-              if ((access() & TurbineFlag.ACC_ANNOTATION) != TurbineFlag.ACC_ANNOTATION) {
-                return null;
-              }
+              RetentionPolicy retention = null;
+              ImmutableSet<ElementType> target = null;
+              ClassSymbol repeatable = null;
               for (ClassFile.AnnotationInfo annotation : classFile.get().annotations()) {
                 switch (annotation.typeName()) {
+                  case "Ljava/lang/annotation/Retention;":
+                    retention = bindRetention(annotation);
+                    break;
                   case "Ljava/lang/annotation/Target;":
-                    return bindTarget(annotation);
+                    target = bindTarget(annotation);
+                    break;
+                  case "Ljava/lang/annotation/Repeatable;":
+                    repeatable = bindRepeatable(annotation);
+                    break;
                   default:
                     break;
                 }
               }
-              EnumSet<ElementType> target = EnumSet.allOf(ElementType.class);
-              target.remove(ElementType.TYPE_USE);
-              target.remove(ElementType.TYPE_PARAMETER);
-              return ImmutableSet.copyOf(target);
+              return new AnnotationMetadata(retention, target, repeatable);
             }
           });
 
+  private RetentionPolicy bindRetention(AnnotationInfo annotation) {
+    ElementValue val = annotation.elementValuePairs().get("value");
+    if (val.kind() != Kind.ENUM) {
+      return null;
+    }
+    EnumConstValue enumVal = (EnumConstValue) val;
+    if (!enumVal.typeName().equals("Ljava/lang/annotation/RetentionPolicy;")) {
+      return null;
+    }
+    return RetentionPolicy.valueOf(enumVal.constName());
+  }
+
   private static ImmutableSet<ElementType> bindTarget(AnnotationInfo annotation) {
     ImmutableSet.Builder<ElementType> result = ImmutableSet.builder();
     ElementValue val = annotation.elementValuePairs().get("value");
@@ -470,9 +459,21 @@
     }
   }
 
+  private static ClassSymbol bindRepeatable(AnnotationInfo annotation) {
+    ElementValue val = annotation.elementValuePairs().get("value");
+    switch (val.kind()) {
+      case CLASS:
+        String className = ((ConstClassValue) val).className();
+        return new ClassSymbol(className.substring(1, className.length() - 1));
+      default:
+        break;
+    }
+    return null;
+  }
+
   @Override
-  public ImmutableSet<ElementType> annotationTarget() {
-    return target.get();
+  public AnnotationMetadata annotationMetadata() {
+    return annotationMetadata.get();
   }
 
   /**
diff --git a/java/com/google/turbine/bytecode/ClassReader.java b/java/com/google/turbine/bytecode/ClassReader.java
index 5311fdc..bcf1c40 100644
--- a/java/com/google/turbine/bytecode/ClassReader.java
+++ b/java/com/google/turbine/bytecode/ClassReader.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue;
+import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.ConstClassValue;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo.ElementValue.EnumConstValue;
 import com.google.turbine.model.Const;
 import com.google.turbine.model.TurbineFlag;
@@ -172,6 +173,7 @@
     switch (annotationType) {
       case "Ljava/lang/annotation/Retention;":
       case "Ljava/lang/annotation/Target;":
+      case "Ljava/lang/annotation/Repeatable;":
         read = true;
         break;
       default:
@@ -193,8 +195,8 @@
   }
 
   /**
-   * Extracts the value of an {@link @Retention} annotation, or else skips over the element value
-   * pair.
+   * Extracts the value of an annotation declaration meta-annotation, or else skips over the element
+   * value pair.
    */
   private ElementValue readElementValue(ConstantPoolReader constantPool, boolean value) {
     int tag = reader.u1();
@@ -228,8 +230,9 @@
           break;
         }
       case 'c':
-        reader.u2(); // classInfoIndex
-        break;
+        int classInfoIndex = reader.u2();
+        String className = constantPool.utf8(classInfoIndex);
+        return new ConstClassValue(className);
       case '@':
         readAnnotation(constantPool);
         break;
diff --git a/java/com/google/turbine/lower/Lower.java b/java/com/google/turbine/lower/Lower.java
index 4da59ad..d7b22fd 100644
--- a/java/com/google/turbine/lower/Lower.java
+++ b/java/com/google/turbine/lower/Lower.java
@@ -16,6 +16,8 @@
 
 package com.google.turbine.lower;
 
+import static com.google.turbine.binder.DisambiguateTypeAnnotations.groupRepeated;
+
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList.Builder;
@@ -356,22 +358,17 @@
       // anything that lexically encloses the class being lowered
       // must be in the same compilation unit, so we have source
       // information for it
-      // TODO(cushon): remove this cast once we're reading type parameters from bytecode
       TypeBoundClass owner = env.get((ClassSymbol) ownerSym);
-      if (!(owner instanceof SourceTypeBoundClass)) {
-        throw new AssertionError(sym);
-      }
-      return ((SourceTypeBoundClass) owner).typeParameterTypes().get(sym);
+      return owner.typeParameterTypes().get(sym);
     }
   }
 
   private ImmutableList<AnnotationInfo> lowerAnnotations(ImmutableList<AnnoInfo> annotations) {
     ImmutableList.Builder<AnnotationInfo> lowered = ImmutableList.builder();
-    outer:
     for (AnnoInfo annotation : annotations) {
       AnnotationInfo anno = lowerAnnotation(annotation);
       if (anno == null) {
-        continue outer;
+        continue;
       }
       lowered.add(anno);
     }
@@ -399,7 +396,7 @@
    */
   @Nullable
   private Boolean isVisible(ClassSymbol sym) {
-    RetentionPolicy retention = env.get(sym).annotationRetention();
+    RetentionPolicy retention = env.get(sym).annotationMetadata().retention();
     switch (retention) {
       case CLASS:
         return false;
@@ -540,7 +537,7 @@
       TargetType boundTargetType) {
     int typeParameterIndex = 0;
     for (TyVarInfo p : typeParameters) {
-      for (AnnoInfo anno : p.annotations()) {
+      for (AnnoInfo anno : groupRepeated(env, p.annotations())) {
         AnnotationInfo info = lowerAnnotation(anno);
         if (info == null) {
           continue;
@@ -621,7 +618,7 @@
 
     /** Lower a list of type annotations. */
     private void lowerTypeAnnotations(ImmutableList<AnnoInfo> annos, TypePath path) {
-      for (AnnoInfo anno : annos) {
+      for (AnnoInfo anno : groupRepeated(env, annos)) {
         AnnotationInfo info = lowerAnnotation(anno);
         if (info == null) {
           continue;
diff --git a/java/com/google/turbine/type/AnnoInfo.java b/java/com/google/turbine/type/AnnoInfo.java
index f821a0f..4af2e66 100644
--- a/java/com/google/turbine/type/AnnoInfo.java
+++ b/java/com/google/turbine/type/AnnoInfo.java
@@ -16,6 +16,8 @@
 
 package com.google.turbine.type;
 
+import static java.util.Objects.requireNonNull;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.turbine.binder.sym.ClassSymbol;
@@ -30,7 +32,7 @@
 
   public AnnoInfo(
       ClassSymbol sym, ImmutableList<Expression> args, ImmutableMap<String, Const> values) {
-    this.sym = sym;
+    this.sym = requireNonNull(sym);
     this.args = args;
     this.values = values;
   }
diff --git a/javatests/com/google/turbine/lower/LowerIntegrationTest.java b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
index b48928c..d2406d9 100644
--- a/javatests/com/google/turbine/lower/LowerIntegrationTest.java
+++ b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
@@ -259,6 +259,7 @@
       "c_array.test",
       "type_anno_retention.test",
       "member_import_clash.test",
+      "anno_repeated.test",
     };
     List<Object[]> tests =
         ImmutableList.copyOf(testCases).stream().map(x -> new Object[] {x}).collect(toList());
diff --git a/javatests/com/google/turbine/lower/LowerTest.java b/javatests/com/google/turbine/lower/LowerTest.java
index 6864a7d..42f395d 100644
--- a/javatests/com/google/turbine/lower/LowerTest.java
+++ b/javatests/com/google/turbine/lower/LowerTest.java
@@ -181,7 +181,6 @@
             null,
             null,
             null,
-            null,
             ImmutableList.of(),
             null);
 
@@ -203,7 +202,6 @@
             null,
             null,
             null,
-            null,
             ImmutableList.of(),
             null);
 
diff --git a/javatests/com/google/turbine/lower/testdata/anno_repeated.test b/javatests/com/google/turbine/lower/testdata/anno_repeated.test
new file mode 100644
index 0000000..928739c
--- /dev/null
+++ b/javatests/com/google/turbine/lower/testdata/anno_repeated.test
@@ -0,0 +1,51 @@
+=== A.java ===
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Target;
+
+@Target({
+  ElementType.TYPE_USE, 
+  ElementType.FIELD, 
+  ElementType.TYPE_PARAMETER,
+  ElementType.PARAMETER,
+})
+@interface As {
+  A[] value() default {};
+}
+
+@Repeatable(As.class)
+@Target({
+  ElementType.TYPE_USE, 
+  ElementType.FIELD, 
+  ElementType.TYPE_PARAMETER,
+  ElementType.PARAMETER,
+})
+@interface A {}
+
+%%% B.java %%%
+import java.lang.annotation.Repeatable;
+
+@interface Bs {
+  B[] value() default {};
+}
+
+@Repeatable(Bs.class)
+@interface B {}
+
+=== C.java ===
+import java.lang.annotation.Repeatable;
+
+@interface Cs {
+  C[] value() default {};
+}
+
+@Repeatable(Cs.class)
+@interface C {}
+
+=== Test.java ===
+import java.util.Map;
+class Test {
+  @A @A @B @B int x;
+  Map<@A @A Integer, @A String> y;
+  <@A @A X> @A X f(@A @A int x) { return null; }
+}