Upgrade turbine to 7c64f0447a967d4717adb7b1b40d8bb856f34186 am: 6f7cfa29c3 am: 278edfeffb am: 5ff202242e am: 820217dbd2 am: ea1cf67d10

Original change: https://android-review.googlesource.com/c/platform/external/turbine/+/2674341

Change-Id: I83c10ceb7d338fd1791d717b833e2c7d589f7704
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index daec318..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-version: 2
-updates:
-  - package-ecosystem: "maven"
-    directory: "/"
-    schedule:
-      interval: "daily"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 54db52c..0bcdf49 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -43,6 +43,9 @@
           - os: ubuntu-latest
             java: 20-ea
             experimental: true
+          - os: ubuntu-latest
+            java: 21-ea
+            experimental: true
     runs-on: ${{ matrix.os }}
     continue-on-error: ${{ matrix.experimental }}
     steps:
diff --git a/METADATA b/METADATA
index ad28c65..841d8e6 100644
--- a/METADATA
+++ b/METADATA
@@ -1,3 +1,7 @@
+# This project was upgraded with external_updater.
+# Usage: tools/external_updater/updater.sh update turbine
+# For more info, check https://cs.android.com/android/platform/superproject/+/main:tools/external_updater/README.md
+
 name: "turbine"
 description: "Turbine is a header compiler for Java."
 third_party {
@@ -9,11 +13,11 @@
     type: GIT
     value: "https://github.com/google/turbine"
   }
-  version: "f42d03f5b18a61a3cdaf2f903e54618771c8797a"
+  version: "7c64f0447a967d4717adb7b1b40d8bb856f34186"
   license_type: NOTICE
   last_upgrade_date {
-    year: 2022
-    month: 6
-    day: 1
+    year: 2023
+    month: 7
+    day: 24
   }
 }
diff --git a/java/com/google/turbine/binder/ClassPathBinder.java b/java/com/google/turbine/binder/ClassPathBinder.java
index 1c41e96..57f30cf 100644
--- a/java/com/google/turbine/binder/ClassPathBinder.java
+++ b/java/com/google/turbine/binder/ClassPathBinder.java
@@ -16,7 +16,6 @@
 
 package com.google.turbine.binder;
 
-import com.google.common.base.Function;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableMap;
@@ -36,6 +35,7 @@
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.function.Function;
 import org.jspecify.nullness.Nullable;
 
 /** Sets up an environment for symbols on the classpath. */
diff --git a/java/com/google/turbine/binder/CompUnitPreprocessor.java b/java/com/google/turbine/binder/CompUnitPreprocessor.java
index 970dc4b..98be898 100644
--- a/java/com/google/turbine/binder/CompUnitPreprocessor.java
+++ b/java/com/google/turbine/binder/CompUnitPreprocessor.java
@@ -36,6 +36,8 @@
 import com.google.turbine.tree.Tree.PkgDecl;
 import com.google.turbine.tree.Tree.TyDecl;
 import com.google.turbine.tree.TurbineModifier;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
@@ -105,7 +107,7 @@
       // "While the file could technically contain the source code
       // for one or more package-private (default-access) classes,
       // it would be very bad form." -- JLS 7.4.1
-      if (!unit.pkg().get().annos().isEmpty()) {
+      if (isPackageInfo(unit)) {
         decls = Iterables.concat(decls, ImmutableList.of(packageInfoTree(unit.pkg().get())));
       }
     } else {
@@ -124,6 +126,18 @@
         unit.imports(), types.build(), unit.mod(), unit.source(), packageName);
   }
 
+  private static boolean isPackageInfo(CompUnit unit) {
+    String path = unit.source().path();
+    if (path == null) {
+      return false;
+    }
+    Path fileName = Paths.get(path).getFileName();
+    if (fileName == null) {
+      return false;
+    }
+    return fileName.toString().equals("package-info.java");
+  }
+
   private static ImmutableMap<String, ClassSymbol> preprocessChildren(
       SourceFile source,
       ImmutableList.Builder<SourceBoundClass> types,
diff --git a/java/com/google/turbine/binder/HierarchyBinder.java b/java/com/google/turbine/binder/HierarchyBinder.java
index ac2c840..3117d4e 100644
--- a/java/com/google/turbine/binder/HierarchyBinder.java
+++ b/java/com/google/turbine/binder/HierarchyBinder.java
@@ -34,6 +34,7 @@
 import com.google.turbine.tree.Tree;
 import com.google.turbine.tree.Tree.ClassTy;
 import java.util.ArrayDeque;
+import java.util.LinkedHashMap;
 import org.jspecify.nullness.Nullable;
 
 /** Type hierarchy binding. */
@@ -109,13 +110,17 @@
       }
     }
 
-    ImmutableMap.Builder<String, TyVarSymbol> typeParameters = ImmutableMap.builder();
+    LinkedHashMap<String, TyVarSymbol> typeParameters = new LinkedHashMap<>();
     for (Tree.TyParam p : decl.typarams()) {
-      typeParameters.put(p.name().value(), new TyVarSymbol(origin, p.name().value()));
+      TyVarSymbol existing =
+          typeParameters.putIfAbsent(p.name().value(), new TyVarSymbol(origin, p.name().value()));
+      if (existing != null) {
+        log.error(p.position(), ErrorKind.DUPLICATE_DECLARATION, p.name());
+      }
     }
 
     return new SourceHeaderBoundClass(
-        base, superclass, interfaces.build(), typeParameters.buildOrThrow());
+        base, superclass, interfaces.build(), ImmutableMap.copyOf(typeParameters));
   }
 
   /**
diff --git a/java/com/google/turbine/binder/Processing.java b/java/com/google/turbine/binder/Processing.java
index 616bf2c..83ee905 100644
--- a/java/com/google/turbine/binder/Processing.java
+++ b/java/com/google/turbine/binder/Processing.java
@@ -19,7 +19,6 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Stopwatch;
 import com.google.common.base.Supplier;
@@ -66,6 +65,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.regex.Pattern;
 import javax.annotation.processing.Processor;
 import javax.lang.model.SourceVersion;
diff --git a/java/com/google/turbine/binder/TypeBinder.java b/java/com/google/turbine/binder/TypeBinder.java
index 92d2827..ec579e7 100644
--- a/java/com/google/turbine/binder/TypeBinder.java
+++ b/java/com/google/turbine/binder/TypeBinder.java
@@ -17,6 +17,7 @@
 package com.google.turbine.binder;
 
 import static com.google.common.collect.Iterables.getLast;
+import static com.google.common.collect.Iterables.getOnlyElement;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Joiner;
@@ -63,6 +64,7 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -204,7 +206,8 @@
         break;
       case CLASS:
         if (base.decl().xtnds().isPresent()) {
-          superClassType = bindClassTy(bindingScope, base.decl().xtnds().get());
+          superClassType =
+              checkClassType(bindingScope, base.decl().xtnds().get(), /* expectInterface= */ false);
         } else if (owner.equals(ClassSymbol.OBJECT)) {
           // java.lang.Object doesn't have a superclass
           superClassType = null;
@@ -226,7 +229,7 @@
     }
 
     for (Tree.ClassTy i : base.decl().impls()) {
-      interfaceTypes.add(bindClassTy(bindingScope, i));
+      interfaceTypes.add(checkClassType(bindingScope, i, /* expectInterface= */ true));
     }
 
     ImmutableList.Builder<ClassSymbol> permits = ImmutableList.builder();
@@ -248,12 +251,16 @@
 
     ImmutableList<RecordComponentInfo> components = bindComponents(scope, base.decl().components());
 
-    ImmutableList.Builder<MethodInfo> methods =
-        ImmutableList.<MethodInfo>builder()
-            .addAll(syntheticMethods(syntheticMethods, components))
-            .addAll(bindMethods(scope, base.decl().members(), components));
+    List<MethodInfo> boundMethods = bindMethods(scope, base.decl().members(), components);
+    ImmutableList<MethodInfo> methods;
     if (base.kind().equals(TurbineTyKind.RECORD)) {
-      methods.addAll(syntheticRecordMethods(syntheticMethods, components));
+      methods = recordMethods(syntheticMethods, components, boundMethods);
+    } else {
+      methods =
+          ImmutableList.<MethodInfo>builder()
+              .addAll(syntheticMethods(syntheticMethods))
+              .addAll(boundMethods)
+              .build();
     }
 
     ImmutableList<FieldInfo> fields = bindFields(scope, base.decl().members());
@@ -265,7 +272,7 @@
         typeParameterTypes,
         base.access(),
         components,
-        methods.build(),
+        methods,
         fields,
         base.owner(),
         base.kind(),
@@ -280,6 +287,193 @@
         base.decl());
   }
 
+  private ImmutableList<MethodInfo> recordMethods(
+      SyntheticMethods syntheticMethods,
+      ImmutableList<RecordComponentInfo> components,
+      List<MethodInfo> boundMethods) {
+    List<MethodInfo> boundConstructors = new ArrayList<>();
+    List<MethodInfo> boundNonConstructors = new ArrayList<>();
+    boolean hasToString = false;
+    boolean hasEquals = false;
+    boolean hasHashCode = false;
+    boolean hasPrimaryConstructor = false;
+    for (MethodInfo m : boundMethods) {
+      if (m.name().equals("<init>")) {
+        if (isPrimaryConstructor(m, components)) {
+          hasPrimaryConstructor = true;
+        }
+        boundConstructors.add(m);
+      } else {
+        switch (m.name()) {
+          case "toString":
+            hasToString = m.parameters().isEmpty();
+            break;
+          case "equals":
+            hasEquals =
+                m.parameters().size() == 1
+                    && hasSameErasure(getOnlyElement(m.parameters()).type(), Type.ClassTy.OBJECT);
+            break;
+          case "hashCode":
+            hasHashCode = m.parameters().isEmpty();
+            break;
+          default: // fall out
+        }
+        boundNonConstructors.add(m);
+      }
+    }
+    ImmutableList.Builder<MethodInfo> methods = ImmutableList.builder();
+    methods.addAll(boundConstructors);
+    if (!hasPrimaryConstructor) {
+      methods.add(defaultRecordConstructor(syntheticMethods, components));
+    }
+    methods.addAll(boundNonConstructors);
+    if (!hasToString) {
+      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));
+    }
+    if (!hasHashCode) {
+      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));
+    }
+    if (!hasEquals) {
+      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 (RecordComponentInfo 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 MethodInfo defaultRecordConstructor(
+      SyntheticMethods syntheticMethods, ImmutableList<RecordComponentInfo> components) {
+    MethodSymbol symbol = syntheticMethods.create(owner, "<init>");
+    ImmutableList.Builder<ParamInfo> params = ImmutableList.builder();
+    for (RecordComponentInfo component : components) {
+      params.add(
+          new ParamInfo(
+              new ParamSymbol(symbol, component.name()),
+              component.type(),
+              component.annotations(),
+              component.access()));
+    }
+    return syntheticConstructor(
+        symbol, params.build(), TurbineVisibility.fromAccess(base.access()));
+  }
+
+  private boolean isPrimaryConstructor(
+      MethodInfo m, ImmutableList<RecordComponentInfo> components) {
+    if (m.parameters().size() != components.size()) {
+      return false;
+    }
+    for (int i = 0; i < m.parameters().size(); i++) {
+      if (!hasSameErasure(m.parameters().get(i).type(), components.get(i).type())) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static boolean hasSameErasure(Type a, Type b) {
+    switch (a.tyKind()) {
+      case PRIM_TY:
+        return b.tyKind() == Type.TyKind.PRIM_TY
+            && ((Type.PrimTy) a).primkind() == ((Type.PrimTy) b).primkind();
+      case CLASS_TY:
+        return b.tyKind() == Type.TyKind.CLASS_TY
+            && ((Type.ClassTy) a).sym().equals(((Type.ClassTy) b).sym());
+      case ARRAY_TY:
+        return b.tyKind() == Type.TyKind.ARRAY_TY
+            && hasSameErasure(((Type.ArrayTy) a).elementType(), ((Type.ArrayTy) b).elementType());
+      case TY_VAR:
+        return b.tyKind() == Type.TyKind.TY_VAR
+            && ((Type.TyVar) a).sym().equals(((Type.TyVar) b).sym());
+      case ERROR_TY:
+        return false;
+      case WILD_TY:
+      case INTERSECTION_TY:
+      case METHOD_TY:
+      case NONE_TY:
+      case VOID_TY:
+        // fall out: impossible method parameter types
+    }
+    throw new AssertionError(a.tyKind());
+  }
+
+  private Type checkClassType(CompoundScope scope, ClassTy tree, boolean expectInterface) {
+    Type type = bindClassTy(scope, tree);
+    if (type.tyKind().equals(Type.TyKind.ERROR_TY)) {
+      return type;
+    }
+    HeaderBoundClass info = env.getNonNull(((Type.ClassTy) type).sym());
+    boolean isInterface;
+    switch (info.kind()) {
+      case INTERFACE:
+      case ANNOTATION:
+        isInterface = true;
+        break;
+      default:
+        isInterface = false;
+        break;
+    }
+    if (expectInterface != isInterface) {
+      log.error(
+          tree.position(),
+          expectInterface ? ErrorKind.EXPECTED_INTERFACE : ErrorKind.UNEXPECTED_INTERFACE);
+    }
+    return type;
+  }
+
   /**
    * A generated for synthetic {@link MethodSymbol}s.
    *
@@ -315,13 +509,10 @@
   }
 
   /** Collect synthetic and implicit methods, including default constructors and enum methods. */
-  ImmutableList<MethodInfo> syntheticMethods(
-      SyntheticMethods syntheticMethods, ImmutableList<RecordComponentInfo> components) {
+  ImmutableList<MethodInfo> syntheticMethods(SyntheticMethods syntheticMethods) {
     switch (base.kind()) {
       case CLASS:
         return maybeDefaultConstructor(syntheticMethods);
-      case RECORD:
-        return maybeDefaultRecordConstructor(syntheticMethods, components);
       case ENUM:
         return syntheticEnumMethods(syntheticMethods);
       default:
@@ -329,25 +520,6 @@
     }
   }
 
-  private ImmutableList<MethodInfo> maybeDefaultRecordConstructor(
-      SyntheticMethods syntheticMethods, ImmutableList<RecordComponentInfo> components) {
-    if (hasConstructor()) {
-      return ImmutableList.of();
-    }
-    MethodSymbol symbol = syntheticMethods.create(owner, "<init>");
-    ImmutableList.Builder<ParamInfo> params = ImmutableList.builder();
-    for (RecordComponentInfo component : components) {
-      params.add(
-          new ParamInfo(
-              new ParamSymbol(symbol, component.name()),
-              component.type(),
-              component.annotations(),
-              component.access()));
-    }
-    return ImmutableList.of(
-        syntheticConstructor(symbol, params.build(), TurbineVisibility.fromAccess(base.access())));
-  }
-
   private ImmutableList<MethodInfo> maybeDefaultConstructor(SyntheticMethods syntheticMethods) {
     if (hasConstructor()) {
       return ImmutableList.of();
@@ -466,71 +638,6 @@
     return methods.build();
   }
 
-  private ImmutableList<MethodInfo> syntheticRecordMethods(
-      SyntheticMethods syntheticMethods, ImmutableList<RecordComponentInfo> 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 (RecordComponentInfo 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) {
@@ -546,7 +653,7 @@
   /** Bind type parameter types. */
   private ImmutableMap<TyVarSymbol, TyVarInfo> bindTyParams(
       ImmutableList<Tree.TyParam> trees, CompoundScope scope, Map<String, TyVarSymbol> symbols) {
-    ImmutableMap.Builder<TyVarSymbol, TyVarInfo> result = ImmutableMap.builder();
+    LinkedHashMap<TyVarSymbol, TyVarInfo> result = new LinkedHashMap<>();
     for (Tree.TyParam tree : trees) {
       // `symbols` is constructed to guarantee the requireNonNull call is safe.
       TyVarSymbol sym = requireNonNull(symbols.get(tree.name().value()));
@@ -555,12 +662,16 @@
         bounds.add(bindTy(scope, bound));
       }
       ImmutableList<AnnoInfo> annotations = bindAnnotations(scope, tree.annos());
-      result.put(
-          sym,
-          new TyVarInfo(
-              IntersectionTy.create(bounds.build()), /* lowerBound= */ null, annotations));
+      TyVarInfo existing =
+          result.putIfAbsent(
+              sym,
+              new TyVarInfo(
+                  IntersectionTy.create(bounds.build()), /* lowerBound= */ null, annotations));
+      if (existing != null) {
+        log.error(tree.position(), ErrorKind.DUPLICATE_DECLARATION, tree.name());
+      }
     }
-    return result.buildOrThrow();
+    return ImmutableMap.copyOf(result);
   }
 
   private List<MethodInfo> bindMethods(
@@ -588,7 +699,8 @@
       for (Tree.TyParam pt : t.typarams()) {
         builder.put(pt.name().value(), new TyVarSymbol(sym, pt.name().value()));
       }
-      typeParameters = builder.buildOrThrow();
+      // errors for duplicates are reported in bindTyParams
+      typeParameters = builder.buildKeepingLast();
     }
 
     // type parameters can refer to each other in f-bounds, so update the scope first
diff --git a/java/com/google/turbine/bytecode/ClassReader.java b/java/com/google/turbine/bytecode/ClassReader.java
index da35196..e73bc49 100644
--- a/java/com/google/turbine/bytecode/ClassReader.java
+++ b/java/com/google/turbine/bytecode/ClassReader.java
@@ -226,9 +226,10 @@
     int unusedLength = reader.u4();
     int numParameters = reader.u1();
     for (int i = 0; i < numParameters; i++) {
-      String name = constantPool.utf8(reader.u2());
+      int nameIndex = reader.u2();
+      String name = nameIndex == 0 ? null : constantPool.utf8(nameIndex);
       int access = reader.u2();
-      if ((access & (TurbineFlag.ACC_SYNTHETIC | TurbineFlag.ACC_MANDATED)) != 0) {
+      if (name == null || (access & (TurbineFlag.ACC_SYNTHETIC | TurbineFlag.ACC_MANDATED)) != 0) {
         // ExecutableElement#getParameters doesn't expect synthetic or mandated
         // parameters
         continue;
diff --git a/java/com/google/turbine/diag/TurbineError.java b/java/com/google/turbine/diag/TurbineError.java
index f839345..8031fa5 100644
--- a/java/com/google/turbine/diag/TurbineError.java
+++ b/java/com/google/turbine/diag/TurbineError.java
@@ -57,6 +57,8 @@
     BAD_MODULE_INFO("unexpected declaration found in module-info"),
     UNCLOSED_COMMENT("unclosed comment"),
     UNEXPECTED_TYPE("unexpected type %s"),
+    EXPECTED_INTERFACE("expected interface type"),
+    UNEXPECTED_INTERFACE("unexpected interface type"),
     UNEXPECTED_MODIFIER("unexpected modifier: %s"),
     PROC("%s");
 
diff --git a/java/com/google/turbine/lower/Lower.java b/java/com/google/turbine/lower/Lower.java
index 362316d..80d8128 100644
--- a/java/com/google/turbine/lower/Lower.java
+++ b/java/com/google/turbine/lower/Lower.java
@@ -21,7 +21,7 @@
 import static java.lang.Math.max;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.base.Function;
+import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -87,35 +87,59 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 import org.jspecify.nullness.Nullable;
 
 /** Lowering from bound classes to bytecode. */
 public class Lower {
 
+  /** Lowering options. */
+  @AutoValue
+  public abstract static class LowerOptions {
+
+    public abstract LanguageVersion languageVersion();
+
+    public abstract boolean emitPrivateFields();
+
+    public static LowerOptions createDefault() {
+      return builder().build();
+    }
+
+    public static Builder builder() {
+      return new AutoValue_Lower_LowerOptions.Builder()
+          .languageVersion(LanguageVersion.createDefault())
+          .emitPrivateFields(false);
+    }
+
+    /** Builder for {@link LowerOptions}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+      public abstract Builder languageVersion(LanguageVersion languageVersion);
+
+      public abstract Builder emitPrivateFields(boolean emitPrivateFields);
+
+      public abstract LowerOptions build();
+    }
+  }
+
   /** The lowered compilation output. */
-  public static class Lowered {
-    private final ImmutableMap<String, byte[]> bytes;
-    private final ImmutableSet<ClassSymbol> symbols;
-
-    public Lowered(ImmutableMap<String, byte[]> bytes, ImmutableSet<ClassSymbol> symbols) {
-      this.bytes = bytes;
-      this.symbols = symbols;
-    }
-
+  @AutoValue
+  public abstract static class Lowered {
     /** Returns the bytecode for classes in the compilation. */
-    public ImmutableMap<String, byte[]> bytes() {
-      return bytes;
-    }
+    public abstract ImmutableMap<String, byte[]> bytes();
 
     /** Returns the set of all referenced symbols in the compilation. */
-    public ImmutableSet<ClassSymbol> symbols() {
-      return symbols;
+    public abstract ImmutableSet<ClassSymbol> symbols();
+
+    public static Lowered create(
+        ImmutableMap<String, byte[]> bytes, ImmutableSet<ClassSymbol> symbols) {
+      return new AutoValue_Lower_Lowered(bytes, symbols);
     }
   }
 
   /** Lowers all given classes to bytecode. */
   public static Lowered lowerAll(
-      LanguageVersion languageVersion,
+      LowerOptions options,
       ImmutableMap<ClassSymbol, SourceTypeBoundClass> units,
       ImmutableList<SourceModuleInfo> modules,
       Env<ClassSymbol, BytecodeBoundClass> classpath) {
@@ -124,9 +148,11 @@
     ImmutableMap.Builder<String, byte[]> result = ImmutableMap.builder();
     Set<ClassSymbol> symbols = new LinkedHashSet<>();
     // Output Java 8 bytecode at minimum, for type annotations
-    int majorVersion = max(languageVersion.majorVersion(), 52);
+    int majorVersion = max(options.languageVersion().majorVersion(), 52);
     for (ClassSymbol sym : units.keySet()) {
-      result.put(sym.binaryName(), lower(units.get(sym), env, sym, symbols, majorVersion));
+      result.put(
+          sym.binaryName(),
+          lower(units.get(sym), env, sym, symbols, majorVersion, options.emitPrivateFields()));
     }
     if (modules.size() == 1) {
       // single module mode: the module-info.class file is at the root
@@ -140,17 +166,18 @@
             lower(module, env, symbols, majorVersion));
       }
     }
-    return new Lowered(result.buildOrThrow(), ImmutableSet.copyOf(symbols));
+    return Lowered.create(result.buildOrThrow(), ImmutableSet.copyOf(symbols));
   }
 
   /** Lowers a class to bytecode. */
-  public static byte[] lower(
+  private static byte[] lower(
       SourceTypeBoundClass info,
       Env<ClassSymbol, TypeBoundClass> env,
       ClassSymbol sym,
       Set<ClassSymbol> symbols,
-      int majorVersion) {
-    return new Lower(env).lower(info, sym, symbols, majorVersion);
+      int majorVersion,
+      boolean emitPrivateFields) {
+    return new Lower(env).lower(info, sym, symbols, majorVersion, emitPrivateFields);
   }
 
   private static byte[] lower(
@@ -251,7 +278,11 @@
   }
 
   private byte[] lower(
-      SourceTypeBoundClass info, ClassSymbol sym, Set<ClassSymbol> symbols, int majorVersion) {
+      SourceTypeBoundClass info,
+      ClassSymbol sym,
+      Set<ClassSymbol> symbols,
+      int majorVersion,
+      boolean emitPrivateFields) {
     int access = classAccess(info);
     String name = sig.descriptor(sym);
     String signature = sig.classSignature(info, env);
@@ -286,8 +317,7 @@
 
     ImmutableList.Builder<ClassFile.FieldInfo> fields = ImmutableList.builder();
     for (FieldInfo f : info.fields()) {
-      if ((f.access() & TurbineFlag.ACC_PRIVATE) == TurbineFlag.ACC_PRIVATE) {
-        // TODO(cushon): drop private members earlier?
+      if (!emitPrivateFields && (f.access() & TurbineFlag.ACC_PRIVATE) == TurbineFlag.ACC_PRIVATE) {
         continue;
       }
       fields.add(lowerField(f));
@@ -568,7 +598,9 @@
 
     private final Map<TyVarSymbol, TyVarInfo> tyParams;
 
-    /** @param tyParams the initial lookup scope, e.g. a method's formal type parameters. */
+    /**
+     * @param tyParams the initial lookup scope, e.g. a method's formal type parameters.
+     */
     public TyVarEnv(Map<TyVarSymbol, TyVarInfo> tyParams) {
       this.tyParams = tyParams;
     }
diff --git a/java/com/google/turbine/main/Main.java b/java/com/google/turbine/main/Main.java
index 34984a8..c246a7a 100644
--- a/java/com/google/turbine/main/Main.java
+++ b/java/com/google/turbine/main/Main.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
@@ -195,13 +196,23 @@
       // TODO(cushon): parallelize
       Lowered lowered =
           Lower.lowerAll(
-              options.languageVersion(), bound.units(), bound.modules(), bound.classPathEnv());
+              Lower.LowerOptions.builder()
+                  .languageVersion(options.languageVersion())
+                  .emitPrivateFields(options.javacOpts().contains("-XDturbine.emitPrivateFields"))
+                  .build(),
+              bound.units(),
+              bound.modules(),
+              bound.classPathEnv());
 
       if (options.outputDeps().isPresent()) {
         DepsProto.Dependencies deps =
             Dependencies.collectDeps(options.targetLabel(), bootclasspath, bound, lowered);
         Path path = Paths.get(options.outputDeps().get());
-        Files.createDirectories(path.getParent());
+        /*
+         * TODO: cpovirk - Consider checking outputDeps for validity earlier so that anyone who
+         * `--output_deps=/` or similar will get a proper error instead of NPE.
+         */
+        Files.createDirectories(requireNonNull(path.getParent()));
         try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(path))) {
           deps.writeTo(os);
         }
@@ -267,7 +278,7 @@
                 /* processorPath= */ options.processorPath(),
                 /* builtinProcessors= */ options.builtinProcessors())),
         bootclasspath,
-        /* moduleVersion=*/ Optional.empty());
+        /* moduleVersion= */ Optional.empty());
   }
 
   private static void usage(TurbineOptions options) {
@@ -314,17 +325,24 @@
   /** Parse all source files and source jars. */
   // TODO(cushon): parallelize
   private static ImmutableList<CompUnit> parseAll(TurbineOptions options) throws IOException {
+    return parseAll(options.sources(), options.sourceJars());
+  }
+
+  static ImmutableList<CompUnit> parseAll(Iterable<String> sources, Iterable<String> sourceJars)
+      throws IOException {
     ImmutableList.Builder<CompUnit> units = ImmutableList.builder();
-    for (String source : options.sources()) {
+    for (String source : sources) {
       Path path = Paths.get(source);
       units.add(Parser.parse(new SourceFile(source, MoreFiles.asCharSource(path, UTF_8).read())));
     }
-    for (String sourceJar : options.sourceJars()) {
-      for (Zip.Entry ze : new Zip.ZipIterable(Paths.get(sourceJar))) {
-        if (ze.name().endsWith(".java")) {
-          String name = ze.name();
-          String source = new String(ze.data(), UTF_8);
-          units.add(Parser.parse(new SourceFile(name, source)));
+    for (String sourceJar : sourceJars) {
+      try (Zip.ZipIterable iterable = new Zip.ZipIterable(Paths.get(sourceJar))) {
+        for (Zip.Entry ze : iterable) {
+          if (ze.name().endsWith(".java")) {
+            String name = ze.name();
+            String source = new String(ze.data(), UTF_8);
+            units.add(Parser.parse(new SourceFile(name, source)));
+          }
         }
       }
     }
@@ -342,7 +360,8 @@
     if (Files.isDirectory(path)) {
       for (SourceFile source : generatedSources.values()) {
         Path to = path.resolve(source.path());
-        Files.createDirectories(to.getParent());
+        // TODO: cpovirk - Consider checking gensrcOutput, similar to outputDeps.
+        Files.createDirectories(requireNonNull(to.getParent()));
         Files.writeString(to, source.source());
       }
       return;
@@ -367,7 +386,8 @@
     if (Files.isDirectory(path)) {
       for (Map.Entry<String, byte[]> resource : generatedResources.entrySet()) {
         Path to = path.resolve(resource.getKey());
-        Files.createDirectories(to.getParent());
+        // TODO: cpovirk - Consider checking resourceOutput, similar to outputDeps.
+        Files.createDirectories(requireNonNull(to.getParent()));
         Files.write(to, resource.getValue());
       }
       return;
diff --git a/java/com/google/turbine/options/TurbineOptions.java b/java/com/google/turbine/options/TurbineOptions.java
index 5cd9a61..007ab23 100644
--- a/java/com/google/turbine/options/TurbineOptions.java
+++ b/java/com/google/turbine/options/TurbineOptions.java
@@ -19,8 +19,8 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.Optional;
-import org.jspecify.nullness.Nullable;
 
 /** Header compilation options. */
 @AutoValue
@@ -182,6 +182,7 @@
 
     abstract ImmutableList.Builder<String> javacOptsBuilder();
 
+    @CanIgnoreReturnValue
     public Builder addAllJavacOpts(Iterable<String> javacOpts) {
       javacOptsBuilder().addAll(javacOpts);
       return this;
@@ -203,11 +204,4 @@
 
     public abstract TurbineOptions build();
   }
-
-  // TODO(b/188833569): remove when AutoValue adds @Nullable to Object if its on the classpath
-  @Override
-  public abstract boolean equals(@Nullable Object other);
-
-  @Override
-  public abstract int hashCode();
 }
diff --git a/java/com/google/turbine/parse/StreamLexer.java b/java/com/google/turbine/parse/StreamLexer.java
index 3d46b90..ed79dd0 100644
--- a/java/com/google/turbine/parse/StreamLexer.java
+++ b/java/com/google/turbine/parse/StreamLexer.java
@@ -75,8 +75,8 @@
     if (result == null) {
       return null;
     }
-    verify(result.endsWith("*/"), result);
-    return result.substring(0, result.length() - "*/".length());
+    verify(result.endsWith("*"), result);
+    return result.substring(0, result.length() - "*".length());
   }
 
   @Override
@@ -153,16 +153,18 @@
                       sawStar = true;
                       break;
                     case '/':
-                      eat();
                       if (sawStar) {
                         if (isJavadoc) {
                           // Save the comment, excluding the leading `/**` and including
                           // the trailing `/*`. The comment is trimmed and normalized later.
                           javadoc = stringValue();
+                          verify(javadoc.endsWith("*"), javadoc);
                         }
+                        eat();
                         continue OUTER;
                       }
                       sawStar = false;
+                      eat();
                       break;
                     case ASCII_SUB:
                       if (reader.done()) {
diff --git a/java/com/google/turbine/processing/TurbineFiler.java b/java/com/google/turbine/processing/TurbineFiler.java
index 8c522ba..bc94870 100644
--- a/java/com/google/turbine/processing/TurbineFiler.java
+++ b/java/com/google/turbine/processing/TurbineFiler.java
@@ -20,7 +20,6 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.base.Function;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableMap;
@@ -44,6 +43,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 import javax.annotation.processing.Filer;
 import javax.annotation.processing.FilerException;
 import javax.lang.model.element.Element;
diff --git a/java/com/google/turbine/processing/TurbineTypes.java b/java/com/google/turbine/processing/TurbineTypes.java
index 467059c..0b69bc3 100644
--- a/java/com/google/turbine/processing/TurbineTypes.java
+++ b/java/com/google/turbine/processing/TurbineTypes.java
@@ -20,7 +20,6 @@
 import static com.google.common.base.Verify.verify;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.turbine.binder.bound.TypeBoundClass;
@@ -50,6 +49,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import javax.lang.model.element.Element;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.type.ArrayType;
diff --git a/java/com/google/turbine/types/Erasure.java b/java/com/google/turbine/types/Erasure.java
index 4b6fbc1..d9c35b3 100644
--- a/java/com/google/turbine/types/Erasure.java
+++ b/java/com/google/turbine/types/Erasure.java
@@ -16,7 +16,6 @@
 
 package com.google.turbine.types;
 
-import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.turbine.binder.bound.TypeBoundClass.TyVarInfo;
@@ -29,6 +28,7 @@
 import com.google.turbine.type.Type.MethodTy;
 import com.google.turbine.type.Type.TyVar;
 import com.google.turbine.type.Type.WildTy;
+import java.util.function.Function;
 
 /** Generic type erasure. */
 public final class Erasure {
diff --git a/javatests/com/google/turbine/binder/BinderErrorTest.java b/javatests/com/google/turbine/binder/BinderErrorTest.java
index 6766470..a1bea05 100644
--- a/javatests/com/google/turbine/binder/BinderErrorTest.java
+++ b/javatests/com/google/turbine/binder/BinderErrorTest.java
@@ -681,7 +681,9 @@
           "class T extends T {}",
         },
         {
-          "<>:1: error: cycle in class hierarchy: T", "class T extends T {}", "                ^",
+          "<>:1: error: cycle in class hierarchy: T", //
+          "class T extends T {}",
+          "                ^",
         },
       },
       {
@@ -692,6 +694,19 @@
           "<>:1: error: cycle in class hierarchy: T",
           "class T implements T {}",
           "                   ^",
+          "<>:1: error: expected interface type",
+          "class T implements T {}",
+          "                   ^",
+        },
+      },
+      {
+        {
+          "interface T extends T {}",
+        },
+        {
+          "<>:1: error: cycle in class hierarchy: T",
+          "interface T extends T {}",
+          "                    ^",
         },
       },
       {
@@ -768,7 +783,7 @@
           "@interface Test {}",
         },
         {
-          "<>:3: error: missing required annotation argument: value",
+          "<>:3: error: missing required annotation argument: value", //
           "@Retention",
           "^",
         },
@@ -958,6 +973,40 @@
           "                                         ^",
         },
       },
+      {
+        {
+          "class C {}", //
+          "interface I {}",
+          "class A extends I implements C {}",
+          "interface B extends C {}",
+        },
+        {
+          "<>:3: error: unexpected interface type",
+          "class A extends I implements C {}",
+          "                ^",
+          "<>:3: error: expected interface type",
+          "class A extends I implements C {}",
+          "                             ^",
+          "<>:4: error: expected interface type",
+          "interface B extends C {}",
+          "                    ^",
+        },
+      },
+      {
+        {
+          "class T<X, X> {", //
+          "  <Y, Y> void f() {}",
+          "}",
+        },
+        {
+          "<>:1: error: duplicate declaration of X",
+          "class T<X, X> {",
+          "           ^",
+          "<>:2: error: duplicate declaration of Y",
+          "  <Y, Y> void f() {}",
+          "      ^",
+        },
+      },
     };
     return Arrays.asList((Object[][]) testCases);
   }
diff --git a/javatests/com/google/turbine/binder/BinderTest.java b/javatests/com/google/turbine/binder/BinderTest.java
index 40387ac..52b769b 100644
--- a/javatests/com/google/turbine/binder/BinderTest.java
+++ b/javatests/com/google/turbine/binder/BinderTest.java
@@ -265,7 +265,7 @@
             parseLines(
                 "import java.lang.annotation.Target;",
                 "import java.lang.annotation.ElementType;",
-                "public class C implements B {",
+                "public class C extends B {",
                 "  @Target(ElementType.TYPE_USE)",
                 "  @interface A {};",
                 "}"));
diff --git a/javatests/com/google/turbine/bytecode/ClassReaderTest.java b/javatests/com/google/turbine/bytecode/ClassReaderTest.java
index ad5b90d..d7abea5 100644
--- a/javatests/com/google/turbine/bytecode/ClassReaderTest.java
+++ b/javatests/com/google/turbine/bytecode/ClassReaderTest.java
@@ -40,6 +40,7 @@
 import org.objectweb.asm.ClassWriter;
 import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.Handle;
+import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.ModuleVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -57,12 +58,16 @@
         null,
         "java/lang/Object",
         null);
-    cw.visitMethod(
-        Opcodes.ACC_PUBLIC,
-        "f",
-        "(Ljava/lang/String;)Ljava/lang/String;",
-        "<T:Ljava/lang/String;>(TT;)TT;",
-        null);
+    MethodVisitor mv =
+        cw.visitMethod(
+            Opcodes.ACC_PUBLIC,
+            "f",
+            "(Ljava/lang/String;)Ljava/lang/String;",
+            "<T:Ljava/lang/String;>(TT;)TT;",
+            null);
+    mv.visitParameter(null, 0); // skip synthetic parameters
+    mv.visitParameter("<no name>", Opcodes.ACC_SYNTHETIC); // skip synthetic parameters
+    mv.visitParameter("parameterName", 42);
     cw.visitMethod(
         Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
         "g",
@@ -92,6 +97,9 @@
     assertThat(f.annotations()).isEmpty();
     assertThat(f.parameterAnnotations()).isEmpty();
     assertThat(f.defaultValue()).isNull();
+    assertThat(f.parameters()).hasSize(1);
+    assertThat(f.parameters().get(0).name()).isEqualTo("parameterName");
+    assertThat(f.parameters().get(0).access()).isEqualTo(42);
 
     ClassFile.MethodInfo g = classFile.methods().get(1);
     assertThat(g.access()).isEqualTo(TurbineFlag.ACC_PUBLIC | TurbineFlag.ACC_STATIC);
diff --git a/javatests/com/google/turbine/bytecode/sig/SigIntegrationTest.java b/javatests/com/google/turbine/bytecode/sig/SigIntegrationTest.java
index 8602fe5..58c0eff 100644
--- a/javatests/com/google/turbine/bytecode/sig/SigIntegrationTest.java
+++ b/javatests/com/google/turbine/bytecode/sig/SigIntegrationTest.java
@@ -23,6 +23,7 @@
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Streams;
+import org.objectweb.asm.Opcodes;
 import java.io.File;
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -44,7 +45,6 @@
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
 
 /**
  * Reads all field, class, and method signatures in the bootclasspath, and round-trips them through
diff --git a/javatests/com/google/turbine/deps/DependenciesTest.java b/javatests/com/google/turbine/deps/DependenciesTest.java
index ba905db..2164a9f 100644
--- a/javatests/com/google/turbine/deps/DependenciesTest.java
+++ b/javatests/com/google/turbine/deps/DependenciesTest.java
@@ -29,7 +29,6 @@
 import com.google.turbine.lower.IntegrationTestSupport;
 import com.google.turbine.lower.Lower;
 import com.google.turbine.lower.Lower.Lowered;
-import com.google.turbine.options.LanguageVersion;
 import com.google.turbine.parse.Parser;
 import com.google.turbine.proto.DepsProto;
 import com.google.turbine.testing.TestClassPaths;
@@ -105,11 +104,11 @@
               units.build(),
               ClassPathBinder.bindClasspath(classpath),
               TestClassPaths.TURBINE_BOOTCLASSPATH,
-              /* moduleVersion=*/ Optional.empty());
+              /* moduleVersion= */ Optional.empty());
 
       Lowered lowered =
           Lower.lowerAll(
-              LanguageVersion.createDefault(),
+              Lower.LowerOptions.createDefault(),
               bound.units(),
               bound.modules(),
               bound.classPathEnv());
diff --git a/javatests/com/google/turbine/lower/IntegrationTestSupport.java b/javatests/com/google/turbine/lower/IntegrationTestSupport.java
index f20962b..6527a03 100644
--- a/javatests/com/google/turbine/lower/IntegrationTestSupport.java
+++ b/javatests/com/google/turbine/lower/IntegrationTestSupport.java
@@ -33,6 +33,7 @@
 import com.google.common.io.MoreFiles;
 import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
+import org.objectweb.asm.Opcodes;
 import com.google.turbine.binder.Binder;
 import com.google.turbine.binder.Binder.BindingResult;
 import com.google.turbine.binder.ClassPath;
@@ -496,7 +497,9 @@
       throws IOException {
     BindingResult bound = turbineAnalysis(input, classpath, bootClassPath, moduleVersion);
     return Lower.lowerAll(
-            LanguageVersion.fromJavacopts(javacopts),
+            Lower.LowerOptions.builder()
+                .languageVersion(LanguageVersion.fromJavacopts(javacopts))
+                .build(),
             bound.units(),
             bound.modules(),
             bound.classPathEnv())
diff --git a/javatests/com/google/turbine/lower/LongStringIntegrationTest.java b/javatests/com/google/turbine/lower/LongStringIntegrationTest.java
index 7bb61e5..33deaee 100644
--- a/javatests/com/google/turbine/lower/LongStringIntegrationTest.java
+++ b/javatests/com/google/turbine/lower/LongStringIntegrationTest.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import org.objectweb.asm.Opcodes;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.file.Path;
@@ -33,7 +34,6 @@
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.FieldVisitor;
-import org.objectweb.asm.Opcodes;
 
 @RunWith(JUnit4.class)
 public class LongStringIntegrationTest {
diff --git a/javatests/com/google/turbine/lower/LowerIntegrationTest.java b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
index 94f1d07..6c95d44 100644
--- a/javatests/com/google/turbine/lower/LowerIntegrationTest.java
+++ b/javatests/com/google/turbine/lower/LowerIntegrationTest.java
@@ -23,6 +23,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import java.io.IOError;
 import java.io.IOException;
@@ -47,6 +48,8 @@
       ImmutableMap.of(
           "record.test", 16, //
           "record2.test", 16,
+          "record_tostring.test", 16,
+          "record_ctor.test", 16,
           "sealed.test", 17,
           "sealed_nested.test", 17,
           "textblock.test", 15);
@@ -269,9 +272,12 @@
       "receiver_param.test",
       "record.test",
       "record2.test",
+      "record_ctor.test",
+      "record_tostring.test",
       "rek.test",
       "samepkg.test",
       "sealed.test",
+      "sealed_nested.test",
       "self.test",
       "semi.test",
       // https://bugs.openjdk.java.net/browse/JDK-8054064 ?
@@ -333,8 +339,9 @@
       "wildcanon.test",
       // keep-sorted end
     };
-    List<Object[]> tests =
-        ImmutableList.copyOf(testCases).stream().map(x -> new Object[] {x}).collect(toList());
+    ImmutableSet<String> cases = ImmutableSet.copyOf(testCases);
+    assertThat(cases).containsAtLeastElementsIn(SOURCE_VERSION.keySet());
+    List<Object[]> tests = cases.stream().map(x -> new Object[] {x}).collect(toList());
     String testShardIndex = System.getenv("TEST_SHARD_INDEX");
     String testTotalShards = System.getenv("TEST_TOTAL_SHARDS");
     if (testShardIndex == null || testTotalShards == null) {
@@ -384,7 +391,12 @@
     int version = SOURCE_VERSION.getOrDefault(test, 8);
     assumeTrue(version <= Runtime.version().feature());
     ImmutableList<String> javacopts =
-        ImmutableList.of("-source", String.valueOf(version), "-target", String.valueOf(version));
+        ImmutableList.of(
+            "-source",
+            String.valueOf(version),
+            "-target",
+            String.valueOf(version),
+            "-Xpkginfo:always");
 
     Map<String, byte[]> expected =
         IntegrationTestSupport.runJavac(input.sources, classpathJar, javacopts);
diff --git a/javatests/com/google/turbine/lower/LowerTest.java b/javatests/com/google/turbine/lower/LowerTest.java
index a6410db..2de4650 100644
--- a/javatests/com/google/turbine/lower/LowerTest.java
+++ b/javatests/com/google/turbine/lower/LowerTest.java
@@ -25,6 +25,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import org.objectweb.asm.Opcodes;
 import com.google.turbine.binder.Binder;
 import com.google.turbine.binder.Binder.BindingResult;
 import com.google.turbine.binder.ClassPathBinder;
@@ -232,7 +233,7 @@
 
     Map<String, byte[]> bytes =
         Lower.lowerAll(
-                LanguageVersion.createDefault(),
+                Lower.LowerOptions.createDefault(),
                 ImmutableMap.of(
                     new ClassSymbol("test/Test"), c, new ClassSymbol("test/Test$Inner"), i),
                 ImmutableList.of(),
@@ -260,10 +261,10 @@
                             "}"))),
             ClassPathBinder.bindClasspath(ImmutableList.of()),
             TURBINE_BOOTCLASSPATH,
-            /* moduleVersion=*/ Optional.empty());
+            /* moduleVersion= */ Optional.empty());
     Map<String, byte[]> lowered =
         Lower.lowerAll(
-                LanguageVersion.createDefault(),
+                Lower.LowerOptions.createDefault(),
                 bound.units(),
                 bound.modules(),
                 bound.classPathEnv())
@@ -340,10 +341,10 @@
                             "}"))),
             ClassPathBinder.bindClasspath(ImmutableList.of()),
             TURBINE_BOOTCLASSPATH,
-            /* moduleVersion=*/ Optional.empty());
+            /* moduleVersion= */ Optional.empty());
     Map<String, byte[]> lowered =
         Lower.lowerAll(
-                LanguageVersion.createDefault(),
+                Lower.LowerOptions.createDefault(),
                 bound.units(),
                 bound.modules(),
                 bound.classPathEnv())
@@ -423,10 +424,10 @@
             ImmutableList.of(Parser.parse("@Deprecated class Test {}")),
             ClassPathBinder.bindClasspath(ImmutableList.of()),
             TURBINE_BOOTCLASSPATH,
-            /* moduleVersion=*/ Optional.empty());
+            /* moduleVersion= */ Optional.empty());
     Map<String, byte[]> lowered =
         Lower.lowerAll(
-                LanguageVersion.createDefault(),
+                Lower.LowerOptions.createDefault(),
                 bound.units(),
                 bound.modules(),
                 bound.classPathEnv())
@@ -544,7 +545,7 @@
             .put(
                 "Test.java",
                 lines(
-                    "public class Test extends B.BM {", //
+                    "public class Test implements B.BM {", //
                     "  I i;",
                     "}"))
             .build();
@@ -649,10 +650,14 @@
             ImmutableList.of(Parser.parse("class Test {}")),
             ClassPathBinder.bindClasspath(ImmutableList.of()),
             TURBINE_BOOTCLASSPATH,
-            /* moduleVersion=*/ Optional.empty());
+            /* moduleVersion= */ Optional.empty());
     Map<String, byte[]> lowered =
         Lower.lowerAll(
-                LanguageVersion.fromJavacopts(ImmutableList.of("-source", "7", "-target", "7")),
+                Lower.LowerOptions.builder()
+                    .languageVersion(
+                        LanguageVersion.fromJavacopts(
+                            ImmutableList.of("-source", "7", "-target", "7")))
+                    .build(),
                 bound.units(),
                 bound.modules(),
                 bound.classPathEnv())
@@ -676,6 +681,76 @@
     assertThat(major[0]).isEqualTo(Opcodes.V1_8);
   }
 
+  @Test
+  public void privateFields() throws Exception {
+    BindingResult bound =
+        Binder.bind(
+            ImmutableList.of(
+                Parser.parse(
+                    "class Test {\n" //
+                        + "  private int x;\n"
+                        + "  int y;\n"
+                        + "}")),
+            ClassPathBinder.bindClasspath(ImmutableList.of()),
+            TURBINE_BOOTCLASSPATH,
+            /* moduleVersion= */ Optional.empty());
+    ImmutableMap<String, byte[]> lowered =
+        Lower.lowerAll(
+                Lower.LowerOptions.builder().emitPrivateFields(true).build(),
+                bound.units(),
+                bound.modules(),
+                bound.classPathEnv())
+            .bytes();
+    List<String> fields = new ArrayList<>();
+    new ClassReader(lowered.get("Test"))
+        .accept(
+            new ClassVisitor(Opcodes.ASM9) {
+              @Override
+              public FieldVisitor visitField(
+                  int access, String name, String descriptor, String signature, Object value) {
+                fields.add(name);
+                return null;
+              }
+            },
+            0);
+    assertThat(fields).containsExactly("x", "y");
+  }
+
+  @Test
+  public void noPrivateFields() throws Exception {
+    BindingResult bound =
+        Binder.bind(
+            ImmutableList.of(
+                Parser.parse(
+                    "class Test {\n" //
+                        + "  private int x;\n"
+                        + "  int y;\n"
+                        + "}")),
+            ClassPathBinder.bindClasspath(ImmutableList.of()),
+            TURBINE_BOOTCLASSPATH,
+            /* moduleVersion= */ Optional.empty());
+    ImmutableMap<String, byte[]> lowered =
+        Lower.lowerAll(
+                Lower.LowerOptions.createDefault(),
+                bound.units(),
+                bound.modules(),
+                bound.classPathEnv())
+            .bytes();
+    List<String> fields = new ArrayList<>();
+    new ClassReader(lowered.get("Test"))
+        .accept(
+            new ClassVisitor(Opcodes.ASM9) {
+              @Override
+              public FieldVisitor visitField(
+                  int access, String name, String descriptor, String signature, Object value) {
+                fields.add(name);
+                return null;
+              }
+            },
+            0);
+    assertThat(fields).containsExactly("y");
+  }
+
   static String lines(String... lines) {
     return Joiner.on(System.lineSeparator()).join(lines);
   }
diff --git a/javatests/com/google/turbine/lower/testdata/package_info.test b/javatests/com/google/turbine/lower/testdata/package_info.test
index a2416a4..6b0fc32 100644
--- a/javatests/com/google/turbine/lower/testdata/package_info.test
+++ b/javatests/com/google/turbine/lower/testdata/package_info.test
@@ -10,3 +10,5 @@
 package p;
 class Test {}
 
+=== Empty.java ===
+package p;
\ No newline at end of file
diff --git a/javatests/com/google/turbine/lower/testdata/record_ctor.test b/javatests/com/google/turbine/lower/testdata/record_ctor.test
new file mode 100644
index 0000000..a3adc15
--- /dev/null
+++ b/javatests/com/google/turbine/lower/testdata/record_ctor.test
@@ -0,0 +1,52 @@
+=== Records.java ===
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+public class Records {
+  public record A(String value) {
+
+    void one() {}
+
+    public A(String a, String b) {
+      this(a + ", " + b);
+    }
+
+    void two() {}
+  }
+
+  @Target(ElementType.TYPE_USE)
+  @interface N {}
+
+  public record B(String value) {
+
+    void one() {}
+
+    public B(@N String value) {
+      this.value = value;
+    }
+
+    void two() {}
+
+    public B(String a, String b) {
+      this(a + ", " + b);
+    }
+
+    void three() {}
+  }
+
+  class Inner {}
+
+  public record C(Records.Inner value) {
+
+    public C(Records. @N Inner value) {
+      this.value = value;
+    }
+  }
+
+  public record D<T>(T value) {
+
+    public D(T value) {
+      this.value = value;
+    }
+  }
+}
diff --git a/javatests/com/google/turbine/lower/testdata/record_tostring.test b/javatests/com/google/turbine/lower/testdata/record_tostring.test
new file mode 100644
index 0000000..f93187a
--- /dev/null
+++ b/javatests/com/google/turbine/lower/testdata/record_tostring.test
@@ -0,0 +1,35 @@
+=== Records.java ===
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+import java.util.Objects;
+
+class Records {
+  public record A() {
+    @Override
+    public String toString() {
+      return "A";
+    }
+  }
+
+  public record B() {
+    @Override
+    public final String toString() {
+      return "B";
+    }
+  }
+
+  public record C() {
+    @Override
+    public final boolean equals(Object o) {
+      return false;
+    }
+  }
+
+  public record D() {
+    @Override
+    public final int hashCode() {
+      return -1;
+    }
+  }
+}
\ No newline at end of file
diff --git a/javatests/com/google/turbine/main/MainTest.java b/javatests/com/google/turbine/main/MainTest.java
index c894d9d..f65e6c0 100644
--- a/javatests/com/google/turbine/main/MainTest.java
+++ b/javatests/com/google/turbine/main/MainTest.java
@@ -106,7 +106,7 @@
 
   @Test
   public void packageInfo() throws IOException {
-    Path src = temporaryFolder.newFile("package-info.jar").toPath();
+    Path src = temporaryFolder.newFile("package-info.java").toPath();
     MoreFiles.asCharSink(src, UTF_8).write("@Deprecated package test;");
 
     Path output = temporaryFolder.newFile("output.jar").toPath();
@@ -467,7 +467,7 @@
 
   @Test
   public void classGeneration() throws IOException {
-    Path src = temporaryFolder.newFile("package-info.jar").toPath();
+    Path src = temporaryFolder.newFile("package-info.java").toPath();
     MoreFiles.asCharSink(src, UTF_8).write("@Deprecated package test;");
     File resources = temporaryFolder.newFile("resources.jar");
     Main.compile(
diff --git a/javatests/com/google/turbine/parse/LexerTest.java b/javatests/com/google/turbine/parse/LexerTest.java
index bf0b374..6a6fe1c 100644
--- a/javatests/com/google/turbine/parse/LexerTest.java
+++ b/javatests/com/google/turbine/parse/LexerTest.java
@@ -339,6 +339,11 @@
     lexerComparisonTest("import pkg\uD800\uDC00.test;");
   }
 
+  @Test
+  public void javadocUnicodeEscape() {
+    lexerComparisonTest("class {/***/\\u007D;");
+  }
+
   private void lexerComparisonTest(String s) {
     assertThat(lex(s)).containsExactlyElementsIn(JavacLexer.javacLex(s));
   }
@@ -349,6 +354,8 @@
     Token token;
     do {
       token = lexer.next();
+      // Just check that javadoc handling doesn't crash
+      String unused = lexer.javadoc();
       String tokenString;
       switch (token) {
         case IDENT:
diff --git a/javatests/com/google/turbine/parse/ParserIntegrationTest.java b/javatests/com/google/turbine/parse/ParserIntegrationTest.java
index c758a74..0981815 100644
--- a/javatests/com/google/turbine/parse/ParserIntegrationTest.java
+++ b/javatests/com/google/turbine/parse/ParserIntegrationTest.java
@@ -20,15 +20,12 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.base.Function;
 import com.google.common.base.Splitter;
-import com.google.common.collect.Iterables;
 import com.google.common.io.CharStreams;
 import com.google.turbine.tree.Tree;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.util.Arrays;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,8 +36,8 @@
 public class ParserIntegrationTest {
 
   @Parameters(name = "{index}: {0}")
-  public static Iterable<Object[]> parameters() {
-    String[] tests = {
+  public static String[] parameters() {
+    return new String[] {
       "anno1.input",
       "anno2.input",
       "annodecl1.input",
@@ -79,14 +76,6 @@
       "record.input",
       "sealed.input",
     };
-    return Iterables.transform(
-        Arrays.asList(tests),
-        new Function<String, Object[]>() {
-          @Override
-          public Object[] apply(String input) {
-            return new Object[] {input};
-          }
-        });
   }
 
   final String input;
diff --git a/javatests/com/google/turbine/processing/TurbineElementsGetAllMembersTest.java b/javatests/com/google/turbine/processing/TurbineElementsGetAllMembersTest.java
index 11dedbf..bc6d9e6 100644
--- a/javatests/com/google/turbine/processing/TurbineElementsGetAllMembersTest.java
+++ b/javatests/com/google/turbine/processing/TurbineElementsGetAllMembersTest.java
@@ -77,17 +77,17 @@
       },
       {
         "=== I.java ===",
-        "abstract class I {",
-        "  abstract Integer f();",
+        "interface I {",
+        "  default Integer f() {}",
         "}",
         "=== J.java ===",
-        "interface J extends I {",
-        "  default Integer f() {",
+        "class J implements I {",
+        "  Integer f() {",
         "    return 42;",
         "  }",
         "}",
         "=== Test.java ===", //
-        "class Test extends I implements J {",
+        "class Test extends J implements I {",
         "}",
       },
       {
diff --git a/javatests/com/google/turbine/processing/TurbineFilerTest.java b/javatests/com/google/turbine/processing/TurbineFilerTest.java
index 96c325b..f76a08d 100644
--- a/javatests/com/google/turbine/processing/TurbineFilerTest.java
+++ b/javatests/com/google/turbine/processing/TurbineFilerTest.java
@@ -21,7 +21,6 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertThrows;
 
-import com.google.common.base.Function;
 import com.google.common.base.Supplier;
 import com.google.common.io.CharStreams;
 import com.google.turbine.diag.SourceFile;
@@ -34,6 +33,7 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 import javax.annotation.processing.FilerException;
 import javax.lang.model.element.Element;
 import javax.tools.FileObject;
diff --git a/pom.xml b/pom.xml
index a2bf088..c96551f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,7 +31,7 @@
 
   <properties>
     <asm.version>9.4</asm.version>
-    <guava.version>31.0.1-jre</guava.version>
+    <guava.version>31.1-jre</guava.version>
     <errorprone.version>2.16</errorprone.version>
     <maven-javadoc-plugin.version>3.3.1</maven-javadoc-plugin.version>
     <maven-source-plugin.version>3.2.1</maven-source-plugin.version>