Initial support for records

PiperOrigin-RevId: 399302681
diff --git a/java/com/google/turbine/binder/CanonicalTypeBinder.java b/java/com/google/turbine/binder/CanonicalTypeBinder.java
index 91fbac7..304005f 100644
--- a/java/com/google/turbine/binder/CanonicalTypeBinder.java
+++ b/java/com/google/turbine/binder/CanonicalTypeBinder.java
@@ -60,6 +60,8 @@
     }
     ImmutableMap<TyVarSymbol, TyVarInfo> typParamTypes =
         typeParameters(base.source(), pos, env, sym, base.typeParameterTypes());
+    ImmutableList<ParamInfo> components =
+        parameters(base.source(), env, sym, pos, base.components());
     ImmutableList<MethodInfo> methods = methods(base.source(), pos, env, sym, base.methods());
     ImmutableList<FieldInfo> fields = fields(base.source(), env, sym, base.fields());
     return new SourceTypeBoundClass(
@@ -67,6 +69,7 @@
         superClassType,
         typParamTypes,
         base.access(),
+        components,
         methods,
         fields,
         base.owner(),
diff --git a/java/com/google/turbine/binder/CompUnitPreprocessor.java b/java/com/google/turbine/binder/CompUnitPreprocessor.java
index 079cd4c..3323938 100644
--- a/java/com/google/turbine/binder/CompUnitPreprocessor.java
+++ b/java/com/google/turbine/binder/CompUnitPreprocessor.java
@@ -176,8 +176,8 @@
         access |= TurbineFlag.ACC_ABSTRACT | TurbineFlag.ACC_INTERFACE | TurbineFlag.ACC_ANNOTATION;
         break;
       case RECORD:
-        // TODO(b/200222393): add support for records
-        throw new AssertionError(tykind);
+        access |= TurbineFlag.ACC_SUPER | TurbineFlag.ACC_FINAL;
+        break;
     }
     return access;
   }
@@ -198,6 +198,7 @@
       case INTERFACE:
       case ENUM:
       case ANNOTATION:
+      case RECORD:
         access |= TurbineFlag.ACC_STATIC;
         break;
       case CLASS:
@@ -205,9 +206,6 @@
           access |= TurbineFlag.ACC_STATIC;
         }
         break;
-      case RECORD:
-        // TODO(b/200222393): add support for records
-        throw new AssertionError(decl.tykind());
     }
 
     // propagate strictfp to nested types
diff --git a/java/com/google/turbine/binder/ConstBinder.java b/java/com/google/turbine/binder/ConstBinder.java
index 5f26ddc..b2c4e1c 100644
--- a/java/com/google/turbine/binder/ConstBinder.java
+++ b/java/com/google/turbine/binder/ConstBinder.java
@@ -104,6 +104,7 @@
                 env,
                 log)
             .evaluateAnnotations(base.annotations());
+    ImmutableList<TypeBoundClass.ParamInfo> components = bindParameters(base.components());
     ImmutableList<TypeBoundClass.FieldInfo> fields = fields(base.fields());
     ImmutableList<MethodInfo> methods = bindMethods(base.methods());
     return new SourceTypeBoundClass(
@@ -111,6 +112,7 @@
         base.superClassType() != null ? bindType(base.superClassType()) : null,
         bindTypeParameters(base.typeParameterTypes()),
         base.access(),
+        components,
         methods,
         fields,
         base.owner(),
diff --git a/java/com/google/turbine/binder/DisambiguateTypeAnnotations.java b/java/com/google/turbine/binder/DisambiguateTypeAnnotations.java
index c5de8c1..0acd0c5 100644
--- a/java/com/google/turbine/binder/DisambiguateTypeAnnotations.java
+++ b/java/com/google/turbine/binder/DisambiguateTypeAnnotations.java
@@ -73,6 +73,7 @@
         base.superClassType(),
         base.typeParameterTypes(),
         base.access(),
+        bindParameters(env, base.components(), TurbineElementType.RECORD_COMPONENT),
         bindMethods(env, base.methods()),
         bindFields(env, base.fields()),
         base.owner(),
@@ -112,33 +113,34 @@
         base.sym(),
         base.tyParams(),
         returnType,
-        bindParameters(env, base.parameters()),
+        bindParameters(env, base.parameters(), TurbineElementType.PARAMETER),
         base.exceptions(),
         base.access(),
         base.defaultValue(),
         base.decl(),
         declarationAnnotations.build(),
-        base.receiver() != null ? bindParam(env, base.receiver()) : null);
+        base.receiver() != null
+            ? bindParam(env, base.receiver(), TurbineElementType.PARAMETER)
+            : null);
   }
 
   private static ImmutableList<ParamInfo> bindParameters(
-      Env<ClassSymbol, TypeBoundClass> env, ImmutableList<ParamInfo> params) {
+      Env<ClassSymbol, TypeBoundClass> env,
+      ImmutableList<ParamInfo> params,
+      TurbineElementType declarationTarget) {
     ImmutableList.Builder<ParamInfo> result = ImmutableList.builder();
     for (ParamInfo param : params) {
-      result.add(bindParam(env, param));
+      result.add(bindParam(env, param, declarationTarget));
     }
     return result.build();
   }
 
-  private static ParamInfo bindParam(Env<ClassSymbol, TypeBoundClass> env, ParamInfo base) {
+  private static ParamInfo bindParam(
+      Env<ClassSymbol, TypeBoundClass> env, ParamInfo base, TurbineElementType declarationTarget) {
     ImmutableList.Builder<AnnoInfo> declarationAnnotations = ImmutableList.builder();
     Type type =
         disambiguate(
-            env,
-            TurbineElementType.PARAMETER,
-            base.type(),
-            base.annotations(),
-            declarationAnnotations);
+            env, declarationTarget, base.type(), base.annotations(), declarationAnnotations);
     return new ParamInfo(base.sym(), type, declarationAnnotations.build(), base.access());
   }
 
diff --git a/java/com/google/turbine/binder/HierarchyBinder.java b/java/com/google/turbine/binder/HierarchyBinder.java
index 2d33613..9a5f763 100644
--- a/java/com/google/turbine/binder/HierarchyBinder.java
+++ b/java/com/google/turbine/binder/HierarchyBinder.java
@@ -83,6 +83,9 @@
         case CLASS:
           superclass = !origin.equals(ClassSymbol.OBJECT) ? ClassSymbol.OBJECT : null;
           break;
+        case RECORD:
+          superclass = ClassSymbol.RECORD;
+          break;
         default:
           throw new AssertionError(decl.tykind());
       }
diff --git a/java/com/google/turbine/binder/TypeBinder.java b/java/com/google/turbine/binder/TypeBinder.java
index ecb8ea7..b28aa24 100644
--- a/java/com/google/turbine/binder/TypeBinder.java
+++ b/java/com/google/turbine/binder/TypeBinder.java
@@ -16,6 +16,7 @@
 
 package com.google.turbine.binder;
 
+import static com.google.common.collect.Iterables.getLast;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Joiner;
@@ -215,6 +216,9 @@
         }
         superClassType = Type.ClassTy.OBJECT;
         break;
+      case RECORD:
+        superClassType = Type.ClassTy.asNonParametricClassTy(ClassSymbol.RECORD);
+        break;
       default:
         throw new AssertionError(base.decl().tykind());
     }
@@ -229,11 +233,18 @@
             .append(new SingletonScope(base.decl().name().value(), owner))
             .append(new ClassMemberScope(owner, env));
 
-    List<MethodInfo> methods =
+    SyntheticMethods syntheticMethods = new SyntheticMethods();
+
+    ImmutableList<ParamInfo> components =
+        bindComponents(scope, syntheticMethods, base.decl().components());
+
+    ImmutableList.Builder<MethodInfo> methods =
         ImmutableList.<MethodInfo>builder()
-            .addAll(syntheticMethods())
-            .addAll(bindMethods(scope, base.decl().members()))
-            .build();
+            .addAll(syntheticMethods(syntheticMethods, components))
+            .addAll(bindMethods(scope, base.decl().members()));
+    if (base.kind().equals(TurbineTyKind.RECORD)) {
+      methods.addAll(syntheticRecordMethods(syntheticMethods, components));
+    }
 
     ImmutableList<FieldInfo> fields = bindFields(scope, base.decl().members());
 
@@ -242,7 +253,8 @@
         superClassType,
         typeParameterTypes,
         base.access(),
-        ImmutableList.copyOf(methods),
+        components,
+        methods.build(),
         fields,
         base.owner(),
         base.kind(),
@@ -257,23 +269,73 @@
         base.decl());
   }
 
+  /**
+   * A generated for synthetic {@link MethodSymbol}s.
+   *
+   * <p>Each {@link MethodSymbol} contains an index into its enclosing class, to enable comparing
+   * the symbols for equality. For synthetic methods we use an arbitrary unique negative index.
+   */
+  private static class SyntheticMethods {
+
+    private int idx = -1;
+
+    MethodSymbol create(ClassSymbol owner, String name) {
+      return new MethodSymbol(idx--, owner, name);
+    }
+  }
+
+  private ImmutableList<ParamInfo> bindComponents(
+      CompoundScope scope,
+      SyntheticMethods syntheticMethods,
+      ImmutableList<Tree.VarDecl> components) {
+    ImmutableList.Builder<ParamInfo> result = ImmutableList.builder();
+    for (Tree.VarDecl p : components) {
+      int access = 0;
+      for (TurbineModifier m : p.mods()) {
+        access |= m.flag();
+      }
+      MethodSymbol msym = syntheticMethods.create(owner, "");
+      ParamInfo param =
+          new ParamInfo(
+              new ParamSymbol(msym, p.name().value()),
+              bindTy(scope, p.ty()),
+              bindAnnotations(scope, p.annos()),
+              access);
+      result.add(param);
+    }
+    return result.build();
+  }
+
   /** Collect synthetic and implicit methods, including default constructors and enum methods. */
-  ImmutableList<MethodInfo> syntheticMethods() {
+  ImmutableList<MethodInfo> syntheticMethods(
+      SyntheticMethods syntheticMethods, ImmutableList<ParamInfo> components) {
     switch (base.kind()) {
       case CLASS:
-        return maybeDefaultConstructor();
+        return maybeDefaultConstructor(syntheticMethods);
+      case RECORD:
+        return maybeDefaultRecordConstructor(syntheticMethods, components);
       case ENUM:
-        return syntheticEnumMethods();
+        return syntheticEnumMethods(syntheticMethods);
       default:
         return ImmutableList.of();
     }
   }
 
-  private ImmutableList<MethodInfo> maybeDefaultConstructor() {
+  private ImmutableList<MethodInfo> maybeDefaultRecordConstructor(
+      SyntheticMethods syntheticMethods, ImmutableList<ParamInfo> components) {
     if (hasConstructor()) {
       return ImmutableList.of();
     }
-    MethodSymbol symbol = new MethodSymbol(-1, owner, "<init>");
+    MethodSymbol symbol = syntheticMethods.create(owner, "<init>");
+    return ImmutableList.of(
+        syntheticConstructor(symbol, components, TurbineVisibility.fromAccess(base.access())));
+  }
+
+  private ImmutableList<MethodInfo> maybeDefaultConstructor(SyntheticMethods syntheticMethods) {
+    if (hasConstructor()) {
+      return ImmutableList.of();
+    }
+    MethodSymbol symbol = syntheticMethods.create(owner, "<init>");
     ImmutableList<ParamInfo> formals;
     if (hasEnclosingInstance(base)) {
       formals = ImmutableList.of(enclosingInstanceParameter(symbol));
@@ -288,6 +350,10 @@
       MethodSymbol symbol, ImmutableList<ParamInfo> formals, TurbineVisibility visibility) {
     int access = visibility.flag();
     access |= (base.access() & TurbineFlag.ACC_STRICT);
+    if (!formals.isEmpty()
+        && (getLast(formals).access() & TurbineFlag.ACC_VARARGS) == TurbineFlag.ACC_VARARGS) {
+      access |= TurbineFlag.ACC_VARARGS;
+    }
     return new MethodInfo(
         symbol,
         ImmutableMap.of(),
@@ -341,15 +407,15 @@
             TurbineFlag.ACC_SYNTHETIC));
   }
 
-  private ImmutableList<MethodInfo> syntheticEnumMethods() {
+  private ImmutableList<MethodInfo> syntheticEnumMethods(SyntheticMethods syntheticMethods) {
     ImmutableList.Builder<MethodInfo> methods = ImmutableList.builder();
     int access = 0;
     access |= (base.access() & TurbineFlag.ACC_STRICT);
     if (!hasConstructor()) {
-      MethodSymbol symbol = new MethodSymbol(-1, owner, "<init>");
+      MethodSymbol symbol = syntheticMethods.create(owner, "<init>");
       methods.add(syntheticConstructor(symbol, enumCtorParams(symbol), TurbineVisibility.PRIVATE));
     }
-    MethodSymbol valuesMethod = new MethodSymbol(-2, owner, "values");
+    MethodSymbol valuesMethod = syntheticMethods.create(owner, "values");
     methods.add(
         new MethodInfo(
             valuesMethod,
@@ -362,7 +428,7 @@
             null,
             ImmutableList.of(),
             null));
-    MethodSymbol valueOfMethod = new MethodSymbol(-3, owner, "valueOf");
+    MethodSymbol valueOfMethod = syntheticMethods.create(owner, "valueOf");
     methods.add(
         new MethodInfo(
             valueOfMethod,
@@ -383,6 +449,71 @@
     return methods.build();
   }
 
+  private ImmutableList<MethodInfo> syntheticRecordMethods(
+      SyntheticMethods syntheticMethods, ImmutableList<ParamInfo> components) {
+    ImmutableList.Builder<MethodInfo> methods = ImmutableList.builder();
+    MethodSymbol toStringMethod = syntheticMethods.create(owner, "toString");
+    methods.add(
+        new MethodInfo(
+            toStringMethod,
+            ImmutableMap.of(),
+            Type.ClassTy.STRING,
+            ImmutableList.of(),
+            ImmutableList.of(),
+            TurbineFlag.ACC_PUBLIC | TurbineFlag.ACC_FINAL,
+            null,
+            null,
+            ImmutableList.of(),
+            null));
+    MethodSymbol hashCodeMethod = syntheticMethods.create(owner, "hashCode");
+    methods.add(
+        new MethodInfo(
+            hashCodeMethod,
+            ImmutableMap.of(),
+            Type.PrimTy.create(TurbineConstantTypeKind.INT, ImmutableList.of()),
+            ImmutableList.of(),
+            ImmutableList.of(),
+            TurbineFlag.ACC_PUBLIC | TurbineFlag.ACC_FINAL,
+            null,
+            null,
+            ImmutableList.of(),
+            null));
+    MethodSymbol equalsMethod = syntheticMethods.create(owner, "equals");
+    methods.add(
+        new MethodInfo(
+            equalsMethod,
+            ImmutableMap.of(),
+            Type.PrimTy.create(TurbineConstantTypeKind.BOOLEAN, ImmutableList.of()),
+            ImmutableList.of(
+                new ParamInfo(
+                    new ParamSymbol(equalsMethod, "other"),
+                    Type.ClassTy.OBJECT,
+                    ImmutableList.of(),
+                    TurbineFlag.ACC_MANDATED)),
+            ImmutableList.of(),
+            TurbineFlag.ACC_PUBLIC | TurbineFlag.ACC_FINAL,
+            null,
+            null,
+            ImmutableList.of(),
+            null));
+    for (ParamInfo c : components) {
+      MethodSymbol componentMethod = syntheticMethods.create(owner, c.name());
+      methods.add(
+          new MethodInfo(
+              componentMethod,
+              ImmutableMap.of(),
+              c.type(),
+              ImmutableList.of(),
+              ImmutableList.of(),
+              TurbineFlag.ACC_PUBLIC,
+              null,
+              null,
+              c.annotations(),
+              null));
+    }
+    return methods.build();
+  }
+
   private boolean hasConstructor() {
     for (Tree m : base.decl().members()) {
       if (m.kind() != Kind.METH_DECL) {
diff --git a/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java b/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
index cadd141..f1ad839 100644
--- a/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
+++ b/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
@@ -44,6 +44,7 @@
   private final ImmutableMap<TyVarSymbol, TyVarInfo> typeParameterTypes;
   private final @Nullable Type superClassType;
   private final ImmutableList<Type> interfaceTypes;
+  private final ImmutableList<ParamInfo> components;
   private final ImmutableList<MethodInfo> methods;
   private final ImmutableList<FieldInfo> fields;
   private final CompoundScope enclosingScope;
@@ -59,6 +60,7 @@
       @Nullable Type superClassType,
       ImmutableMap<TyVarSymbol, TyVarInfo> typeParameterTypes,
       int access,
+      ImmutableList<ParamInfo> components,
       ImmutableList<MethodInfo> methods,
       ImmutableList<FieldInfo> fields,
       @Nullable ClassSymbol owner,
@@ -76,6 +78,7 @@
     this.superClassType = superClassType;
     this.typeParameterTypes = typeParameterTypes;
     this.access = access;
+    this.components = components;
     this.methods = methods;
     this.fields = fields;
     this.owner = owner;
@@ -149,6 +152,12 @@
     return superClassType;
   }
 
+  /** The record components. */
+  @Override
+  public ImmutableList<ParamInfo> components() {
+    return components;
+  }
+
   /** Declared methods. */
   @Override
   public ImmutableList<MethodInfo> methods() {
diff --git a/java/com/google/turbine/binder/bound/TypeBoundClass.java b/java/com/google/turbine/binder/bound/TypeBoundClass.java
index 1d7a4cc..14f4f2d 100644
--- a/java/com/google/turbine/binder/bound/TypeBoundClass.java
+++ b/java/com/google/turbine/binder/bound/TypeBoundClass.java
@@ -16,7 +16,6 @@
 
 package com.google.turbine.binder.bound;
 
-
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.turbine.binder.sym.FieldSymbol;
@@ -51,6 +50,9 @@
   /** Declared methods. */
   ImmutableList<MethodInfo> methods();
 
+  /** Record components. */
+  ImmutableList<ParamInfo> components();
+
   /**
    * Annotation metadata, e.g. from {@link java.lang.annotation.Target}, {@link
    * java.lang.annotation.Retention}, and {@link java.lang.annotation.Repeatable}.
diff --git a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
index 7317ba4..1f196f5 100644
--- a/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
+++ b/java/com/google/turbine/binder/bytecode/BytecodeBoundClass.java
@@ -486,6 +486,11 @@
     return methods.get();
   }
 
+  @Override
+  public ImmutableList<ParamInfo> components() {
+    return ImmutableList.of();
+  }
+
   private final Supplier<@Nullable AnnotationMetadata> annotationMetadata =
       Suppliers.memoize(
           new Supplier<@Nullable AnnotationMetadata>() {
diff --git a/java/com/google/turbine/binder/sym/ClassSymbol.java b/java/com/google/turbine/binder/sym/ClassSymbol.java
index 45c9adf..9bb556f 100644
--- a/java/com/google/turbine/binder/sym/ClassSymbol.java
+++ b/java/com/google/turbine/binder/sym/ClassSymbol.java
@@ -33,6 +33,7 @@
   public static final ClassSymbol OBJECT = new ClassSymbol("java/lang/Object");
   public static final ClassSymbol STRING = new ClassSymbol("java/lang/String");
   public static final ClassSymbol ENUM = new ClassSymbol("java/lang/Enum");
+  public static final ClassSymbol RECORD = new ClassSymbol("java/lang/Record");
   public static final ClassSymbol ANNOTATION = new ClassSymbol("java/lang/annotation/Annotation");
   public static final ClassSymbol INHERITED = new ClassSymbol("java/lang/annotation/Inherited");
   public static final ClassSymbol CLONEABLE = new ClassSymbol("java/lang/Cloneable");
diff --git a/java/com/google/turbine/lower/Lower.java b/java/com/google/turbine/lower/Lower.java
index 9b2d49f..d4d02bd 100644
--- a/java/com/google/turbine/lower/Lower.java
+++ b/java/com/google/turbine/lower/Lower.java
@@ -259,6 +259,16 @@
       interfaces.add(sig.descriptor(i));
     }
 
+    ClassFile.RecordInfo record = null;
+    if (info.kind().equals(TurbineTyKind.RECORD)) {
+      ImmutableList.Builder<ClassFile.RecordInfo.RecordComponentInfo> components =
+          ImmutableList.builder();
+      for (ParamInfo component : info.components()) {
+        components.add(lowerComponent(info, component));
+      }
+      record = new ClassFile.RecordInfo(components.build());
+    }
+
     List<ClassFile.MethodInfo> methods = new ArrayList<>();
     for (MethodInfo m : info.methods()) {
       if (TurbineVisibility.fromAccess(m.access()) == TurbineVisibility.PRIVATE) {
@@ -281,6 +291,14 @@
 
     ImmutableList<TypeAnnotationInfo> typeAnnotations = classTypeAnnotations(info);
 
+    String nestHost = null;
+    ImmutableList<String> nestMembers = ImmutableList.of();
+    // nests were added in Java 11, i.e. major version 55
+    if (majorVersion >= 55) {
+      nestHost = collectNestHost(info.source(), info.owner());
+      nestMembers = nestHost == null ? collectNestMembers(info.source(), info) : ImmutableList.of();
+    }
+
     ImmutableList<ClassFile.InnerClass> inners = collectInnerClasses(info.source(), sym, info);
 
     ClassFile classfile =
@@ -297,9 +315,9 @@
             inners,
             typeAnnotations,
             /* module= */ null,
-            /* nestHost= */ null,
-            /* nestMembers= */ ImmutableList.of(),
-            /* record= */ null,
+            nestHost,
+            nestMembers,
+            record,
             /* transitiveJar= */ null);
 
     symbols.addAll(sig.classes);
@@ -307,6 +325,18 @@
     return ClassWriter.writeClass(classfile);
   }
 
+  private ClassFile.RecordInfo.RecordComponentInfo lowerComponent(
+      SourceTypeBoundClass info, ParamInfo c) {
+    Function<TyVarSymbol, TyVarInfo> tenv = new TyVarEnv(info.typeParameterTypes());
+    String desc = SigWriter.type(sig.signature(Erasure.erase(c.type(), tenv)));
+    String signature = sig.fieldSignature(c.type());
+    ImmutableList.Builder<TypeAnnotationInfo> typeAnnotations = ImmutableList.builder();
+    lowerTypeAnnotations(
+        typeAnnotations, c.type(), TargetType.FIELD, TypeAnnotationInfo.EMPTY_TARGET);
+    return new ClassFile.RecordInfo.RecordComponentInfo(
+        c.name(), desc, signature, lowerAnnotations(c.annotations()), typeAnnotations.build());
+  }
+
   private ClassFile.MethodInfo lowerMethod(final MethodInfo m, final ClassSymbol sym) {
     int access = m.access();
     Function<TyVarSymbol, TyVarInfo> tenv = new TyVarEnv(m.tyParams());
@@ -440,13 +470,58 @@
     if (info == null) {
       throw TurbineError.format(source, ErrorKind.CLASS_FILE_NOT_FOUND, sym);
     }
-    ClassSymbol owner = env.getNonNull(sym).owner();
+    ClassSymbol owner = info.owner();
     if (owner != null) {
       addEnclosing(source, env, all, owner);
       all.add(sym);
     }
   }
 
+  private @Nullable String collectNestHost(SourceFile source, @Nullable ClassSymbol sym) {
+    if (sym == null) {
+      return null;
+    }
+    while (true) {
+      TypeBoundClass info = env.get(sym);
+      if (info == null) {
+        throw TurbineError.format(source, ErrorKind.CLASS_FILE_NOT_FOUND, sym);
+      }
+      if (info.owner() == null) {
+        return sig.descriptor(sym);
+      }
+      sym = info.owner();
+    }
+  }
+
+  private ImmutableList<String> collectNestMembers(SourceFile source, SourceTypeBoundClass info) {
+    Set<ClassSymbol> nestMembers = new LinkedHashSet<>();
+    for (ClassSymbol child : info.children().values()) {
+      addNestMembers(source, env, nestMembers, child);
+    }
+    ImmutableList.Builder<String> result = ImmutableList.builder();
+    for (ClassSymbol nestMember : nestMembers) {
+      result.add(sig.descriptor(nestMember));
+    }
+    return result.build();
+  }
+
+  private static void addNestMembers(
+      SourceFile source,
+      Env<ClassSymbol, TypeBoundClass> env,
+      Set<ClassSymbol> nestMembers,
+      ClassSymbol sym) {
+    if (!nestMembers.add(sym)) {
+      return;
+    }
+    TypeBoundClass info = env.get(sym);
+    if (info == null) {
+      throw TurbineError.format(source, ErrorKind.CLASS_FILE_NOT_FOUND, sym);
+    }
+    for (ClassSymbol child : info.children().values()) {
+      addNestMembers(source, env, nestMembers, child);
+    }
+  }
+
   /**
    * Creates an inner class attribute, given an inner class that was referenced somewhere in the
    * class.
diff --git a/java/com/google/turbine/model/TurbineElementType.java b/java/com/google/turbine/model/TurbineElementType.java
index a68df3a..a7debf3 100644
--- a/java/com/google/turbine/model/TurbineElementType.java
+++ b/java/com/google/turbine/model/TurbineElementType.java
@@ -28,5 +28,6 @@
   PARAMETER,
   TYPE,
   TYPE_PARAMETER,
-  TYPE_USE
+  TYPE_USE,
+  RECORD_COMPONENT
 }
diff --git a/java/com/google/turbine/processing/TurbineElement.java b/java/com/google/turbine/processing/TurbineElement.java
index 69f6ec0..8581fda 100644
--- a/java/com/google/turbine/processing/TurbineElement.java
+++ b/java/com/google/turbine/processing/TurbineElement.java
@@ -379,7 +379,7 @@
         case ANNOTATION:
           return ElementKind.ANNOTATION_TYPE;
         case RECORD:
-          // TODO(b/200222393): add support for records
+          return ElementKind.valueOf("RECORD");
       }
       throw new AssertionError(info.kind());
     }
diff --git a/javatests/com/google/turbine/lower/LowerIntegrationTest.java b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
index f832314..389f55a 100644
--- a/javatests/com/google/turbine/lower/LowerIntegrationTest.java
+++ b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
@@ -16,14 +16,19 @@
 
 package com.google.turbine.lower;
 
+import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_VERSION;
+import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.turbine.testing.TestResources.getResource;
 import static java.util.stream.Collectors.toList;
+import static org.junit.Assume.assumeTrue;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import java.io.IOError;
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -41,6 +46,9 @@
 @RunWith(Parameterized.class)
 public class LowerIntegrationTest {
 
+  private static final ImmutableMap<String, Integer> SOURCE_VERSION =
+      ImmutableMap.of("record.test", 16);
+
   @Parameters(name = "{index}: {0}")
   public static Iterable<Object[]> parameters() {
     String[] testCases = {
@@ -256,6 +264,7 @@
       "rawcanon.test",
       "rawfbound.test",
       "receiver_param.test",
+      "record.test",
       "rek.test",
       "samepkg.test",
       "self.test",
@@ -366,11 +375,34 @@
       classpathJar = ImmutableList.of(lib);
     }
 
-    Map<String, byte[]> expected = IntegrationTestSupport.runJavac(input.sources, classpathJar);
+    int version = SOURCE_VERSION.getOrDefault(test, 8);
+    assumeTrue(version <= getMajor());
+    ImmutableList<String> javacopts =
+        ImmutableList.of("-source", String.valueOf(version), "-target", String.valueOf(version));
 
-    Map<String, byte[]> actual = IntegrationTestSupport.runTurbine(input.sources, classpathJar);
+    Map<String, byte[]> expected =
+        IntegrationTestSupport.runJavac(input.sources, classpathJar, javacopts);
+
+    Map<String, byte[]> actual =
+        IntegrationTestSupport.runTurbine(input.sources, classpathJar, javacopts);
 
     assertThat(IntegrationTestSupport.dump(IntegrationTestSupport.sortMembers(actual)))
         .isEqualTo(IntegrationTestSupport.dump(IntegrationTestSupport.canonicalize(expected)));
   }
+
+  private static int getMajor() {
+    try {
+      Method versionMethod = Runtime.class.getMethod("version");
+      Object version = versionMethod.invoke(null);
+      return (int) version.getClass().getMethod("major").invoke(version);
+    } catch (ReflectiveOperationException e) {
+      // continue below
+    }
+
+    int version = (int) Double.parseDouble(JAVA_CLASS_VERSION.value());
+    if (49 <= version && version <= 52) {
+      return version - (49 - 5);
+    }
+    throw new IllegalStateException("Unknown Java version: " + JAVA_SPECIFICATION_VERSION.value());
+  }
 }
diff --git a/javatests/com/google/turbine/lower/LowerTest.java b/javatests/com/google/turbine/lower/LowerTest.java
index 67fb549..e560321 100644
--- a/javatests/com/google/turbine/lower/LowerTest.java
+++ b/javatests/com/google/turbine/lower/LowerTest.java
@@ -188,6 +188,7 @@
             xtnds,
             tps,
             access,
+            ImmutableList.of(),
             methods,
             fields,
             owner,
@@ -210,6 +211,7 @@
             TurbineFlag.ACC_STATIC | TurbineFlag.ACC_PROTECTED,
             ImmutableList.of(),
             ImmutableList.of(),
+            ImmutableList.of(),
             new ClassSymbol("test/Test"),
             TurbineTyKind.CLASS,
             ImmutableMap.of("Inner", new ClassSymbol("test/Test$Inner")),
diff --git a/javatests/com/google/turbine/lower/testdata/record.test b/javatests/com/google/turbine/lower/testdata/record.test
new file mode 100644
index 0000000..7d92c2b
--- /dev/null
+++ b/javatests/com/google/turbine/lower/testdata/record.test
@@ -0,0 +1,46 @@
+=== Records.java ===
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+import java.util.List;
+
+class Records {
+  record R1() {}
+
+  private record R2() {}
+
+  @Deprecated
+  private record R3() {}
+
+  record R4<T>() {}
+
+  record R5<T>(int x) {}
+
+  record R6<T>(@Deprecated int x) {}
+
+  record R7<T>(@Deprecated int x, int... y) {}
+
+  record R8<T>() implements Comparable<R8<T>> {
+    @Override
+    public int compareTo(R8<T> other) {
+      return 0;
+    }
+  }
+
+  record R9(int x) {
+    R9(int x) {
+      this.x = x;
+    }
+  }
+
+  @Target(ElementType.TYPE_USE)
+  @interface A {}
+
+  @Target(ElementType.RECORD_COMPONENT)
+  @interface B {}
+
+  @Target({ElementType.TYPE_USE, ElementType.RECORD_COMPONENT})
+  @interface C {}
+
+  record R10<T>(@A List<@A T> x, @B int y, @C int z) {
+  }
+}