Bound nodes with types, and initial lowering to bytecode

Add the model used by all remaining binding phases, and lowering from
that model to bytecode.

MOE_MIGRATED_REVID=135135482
diff --git a/java/com/google/turbine/binder/ClassPathBinder.java b/java/com/google/turbine/binder/ClassPathBinder.java
index 7b6b830..73609fc 100644
--- a/java/com/google/turbine/binder/ClassPathBinder.java
+++ b/java/com/google/turbine/binder/ClassPathBinder.java
@@ -39,7 +39,7 @@
    * Creates an environment containing symbols in the given classpath and bootclasspath, and adds
    * them to the top-level index.
    */
-  static CompoundEnv<BytecodeBoundClass> bind(
+  public static CompoundEnv<BytecodeBoundClass> bind(
       Iterable<Path> classpath, Iterable<Path> bootclasspath, TopLevelIndex.Builder tli)
       throws IOException {
     // TODO(cushon): this is going to require an env eventually,
diff --git a/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java b/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
new file mode 100644
index 0000000..3518278
--- /dev/null
+++ b/java/com/google/turbine/binder/bound/SourceTypeBoundClass.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.turbine.binder.bound;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.turbine.binder.sym.ClassSymbol;
+import com.google.turbine.binder.sym.FieldSymbol;
+import com.google.turbine.binder.sym.MethodSymbol;
+import com.google.turbine.binder.sym.TyVarSymbol;
+import com.google.turbine.model.TurbineTyKind;
+import com.google.turbine.type.Type;
+import javax.annotation.Nullable;
+
+/** A HeaderBoundClass for classes compiled from source. */
+public class SourceTypeBoundClass implements HeaderBoundClass {
+
+  private final TurbineTyKind kind;
+  private final ClassSymbol owner;
+  private final ImmutableMap<String, ClassSymbol> children;
+
+  private final int access;
+  private final ClassSymbol superclass;
+  private final ImmutableList<ClassSymbol> interfaces;
+  private final ImmutableMap<String, TyVarSymbol> typeParameters;
+
+  private final ImmutableMap<TyVarSymbol, TyVarInfo> typeParameterTypes;
+  private final Type.ClassTy superClassType;
+  private final ImmutableList<Type.ClassTy> interfaceTypes;
+  private final ImmutableList<MethodInfo> methods;
+  private final ImmutableList<FieldInfo> fields;
+
+  public SourceTypeBoundClass(
+      ImmutableList<Type.ClassTy> interfaceTypes,
+      Type.ClassTy superClassType,
+      ImmutableMap<TyVarSymbol, TyVarInfo> typeParameterTypes,
+      int access,
+      ImmutableList<MethodInfo> methods,
+      ImmutableList<FieldInfo> fields,
+      ClassSymbol owner,
+      TurbineTyKind kind,
+      ImmutableMap<String, ClassSymbol> children,
+      ClassSymbol superclass,
+      ImmutableList<ClassSymbol> interfaces,
+      ImmutableMap<String, TyVarSymbol> typeParameters) {
+    this.interfaceTypes = interfaceTypes;
+    this.superClassType = superClassType;
+    this.typeParameterTypes = typeParameterTypes;
+    this.access = access;
+    this.methods = methods;
+    this.fields = fields;
+    this.owner = owner;
+    this.kind = kind;
+    this.children = children;
+    this.superclass = superclass;
+    this.interfaces = interfaces;
+    this.typeParameters = typeParameters;
+  }
+
+  @Override
+  public ClassSymbol superclass() {
+    return superclass;
+  }
+
+  @Override
+  public ImmutableList<ClassSymbol> interfaces() {
+    return interfaces;
+  }
+
+  @Override
+  public int access() {
+    return access;
+  }
+
+  @Override
+  public TurbineTyKind kind() {
+    return kind;
+  }
+
+  @Nullable
+  @Override
+  public ClassSymbol owner() {
+    return owner;
+  }
+
+  @Override
+  public ImmutableMap<String, ClassSymbol> children() {
+    return children;
+  }
+
+  /** Declared type parameters. */
+  public ImmutableMap<String, TyVarSymbol> typeParameters() {
+    return typeParameters;
+  }
+
+  /** Implemented interface types. */
+  public ImmutableList<Type.ClassTy> interfaceTypes() {
+    return interfaceTypes;
+  }
+
+  /** The super-class type. */
+  public Type.ClassTy superClassType() {
+    return superClassType;
+  }
+
+  /** Declared methods. */
+  public ImmutableList<MethodInfo> methods() {
+    return methods;
+  }
+
+  /** Declared fields. */
+  public ImmutableList<FieldInfo> fields() {
+    return fields;
+  }
+
+  /** Declared type parameters. */
+  public ImmutableMap<TyVarSymbol, TyVarInfo> typeParameterTypes() {
+    return typeParameterTypes;
+  }
+
+  /** A declared method. */
+  public static class MethodInfo {
+    private final MethodSymbol sym;
+    private final ImmutableMap<TyVarSymbol, TyVarInfo> tyParams;
+    private final Type returnType;
+    private final ImmutableList<ParamInfo> parameters;
+    private final ImmutableList<Type> exceptions;
+    private final int access;
+
+    public MethodInfo(
+        MethodSymbol sym,
+        ImmutableMap<TyVarSymbol, TyVarInfo> tyParams,
+        Type returnType,
+        ImmutableList<ParamInfo> parameters,
+        ImmutableList<Type> exceptions,
+        int access) {
+      this.sym = sym;
+      this.tyParams = tyParams;
+      this.returnType = returnType;
+      this.parameters = parameters;
+      this.exceptions = exceptions;
+      this.access = access;
+    }
+
+    /** The method symbol. */
+    public MethodSymbol sym() {
+      return sym;
+    }
+
+    /** The method name. */
+    public String name() {
+      return sym.name();
+    }
+
+    /** The type parameters */
+    public ImmutableMap<TyVarSymbol, TyVarInfo> tyParams() {
+      return tyParams;
+    }
+
+    /** Type return type, possibly {#link Type#VOID}. */
+    public Type returnType() {
+      return returnType;
+    }
+
+    /** The formal parameters. */
+    public ImmutableList<ParamInfo> parameters() {
+      return parameters;
+    }
+
+    /** Thrown exceptions. */
+    public ImmutableList<Type> exceptions() {
+      return exceptions;
+    }
+
+    /** Access bits. */
+    public int access() {
+      return access;
+    }
+  }
+
+  /** A formal parameter declaration. */
+  public static class ParamInfo {
+    private final Type type;
+    private final boolean synthetic;
+
+    public ParamInfo(Type type, boolean synthetic) {
+      this.type = type;
+      this.synthetic = synthetic;
+    }
+
+    /** The parameter type. */
+    public Type type() {
+      return type;
+    }
+
+    /**
+     * Returns true if the parameter is synthetic, e.g. the enclosing instance parameter in an inner
+     * class constructor.
+     */
+    public boolean synthetic() {
+      return synthetic;
+    }
+  }
+
+  /** A field declaration. */
+  public static class FieldInfo {
+    private final FieldSymbol sym;
+    private final Type type;
+    private final int access;
+
+    public FieldInfo(FieldSymbol sym, Type type, int access) {
+      this.sym = sym;
+      this.type = type;
+      this.access = access;
+    }
+
+    /** The field symbol. */
+    public FieldSymbol sym() {
+      return sym;
+    }
+
+    /** The field name. */
+    public String name() {
+      return sym.name();
+    }
+
+    /** The field type. */
+    public Type type() {
+      return type;
+    }
+
+    /** Access bits. */
+    public int access() {
+      return access;
+    }
+  }
+
+  /** A type parameter declaration. */
+  public static class TyVarInfo {
+    private final Type superClassBound;
+    private final ImmutableList<Type> interfaceBounds;
+
+    public TyVarInfo(Type superClassBound, ImmutableList<Type> interfaceBounds) {
+      this.superClassBound = superClassBound;
+      this.interfaceBounds = interfaceBounds;
+    }
+
+    /** A class bound, or {@code null}. */
+    public Type superClassBound() {
+      return superClassBound;
+    }
+
+    /** Interface type bounds. */
+    public ImmutableList<Type> interfaceBounds() {
+      return interfaceBounds;
+    }
+  }
+}
diff --git a/java/com/google/turbine/binder/sym/FieldSymbol.java b/java/com/google/turbine/binder/sym/FieldSymbol.java
new file mode 100644
index 0000000..9ff8fb1
--- /dev/null
+++ b/java/com/google/turbine/binder/sym/FieldSymbol.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.turbine.binder.sym;
+
+import com.google.errorprone.annotations.Immutable;
+import java.util.Objects;
+
+/** A field symbol. */
+@Immutable
+public class FieldSymbol implements Symbol {
+  private final ClassSymbol owner;
+  private final String name;
+
+  public FieldSymbol(ClassSymbol owner, String name) {
+    this.owner = owner;
+    this.name = name;
+  }
+
+  /** The enclosing class. */
+  public ClassSymbol owner() {
+    return owner;
+  }
+
+  /** The field name. */
+  public String name() {
+    return name;
+  }
+
+  @Override
+  public Kind symKind() {
+    return Kind.FIELD;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, owner);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof FieldSymbol)) {
+      return false;
+    }
+    FieldSymbol other = (FieldSymbol) obj;
+    return name().equals(other.name()) && owner().equals(other.owner());
+  }
+}
diff --git a/java/com/google/turbine/binder/sym/MethodSymbol.java b/java/com/google/turbine/binder/sym/MethodSymbol.java
new file mode 100644
index 0000000..17459b4
--- /dev/null
+++ b/java/com/google/turbine/binder/sym/MethodSymbol.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.turbine.binder.sym;
+
+import com.google.errorprone.annotations.Immutable;
+import java.util.Objects;
+
+/** A method symbol. */
+@Immutable
+public class MethodSymbol implements Symbol {
+  private final ClassSymbol owner;
+  private final String name;
+
+  public MethodSymbol(ClassSymbol owner, String name) {
+    this.owner = owner;
+    this.name = name;
+  }
+
+  /** The enclosing class. */
+  public ClassSymbol owner() {
+    return owner;
+  }
+
+  /** The method name. */
+  public String name() {
+    return name;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, owner);
+  }
+
+  @Override
+  public Kind symKind() {
+    return Kind.METHOD;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof MethodSymbol)) {
+      return false;
+    }
+    MethodSymbol other = (MethodSymbol) obj;
+    return name().equals(other.name()) && owner().equals(other.owner());
+  }
+}
diff --git a/java/com/google/turbine/binder/sym/Symbol.java b/java/com/google/turbine/binder/sym/Symbol.java
index 0014aae..51b5fe3 100644
--- a/java/com/google/turbine/binder/sym/Symbol.java
+++ b/java/com/google/turbine/binder/sym/Symbol.java
@@ -16,13 +16,17 @@
 
 package com.google.turbine.binder.sym;
 
+import com.google.errorprone.annotations.Immutable;
+
 /** The top interface for all symbols. */
+@Immutable
 public interface Symbol {
   /** The symbol kind. */
   enum Kind {
     CLASS,
-    TY_PARAM
-    // TODO(cushon): FIELD, METHOD
+    TY_PARAM,
+    METHOD,
+    FIELD
   }
 
   /** The symbol kind. */
diff --git a/java/com/google/turbine/binder/sym/TyVarSymbol.java b/java/com/google/turbine/binder/sym/TyVarSymbol.java
index 91e4985..9276f91 100644
--- a/java/com/google/turbine/binder/sym/TyVarSymbol.java
+++ b/java/com/google/turbine/binder/sym/TyVarSymbol.java
@@ -16,7 +16,11 @@
 
 package com.google.turbine.binder.sym;
 
+import com.google.errorprone.annotations.Immutable;
+import java.util.Objects;
+
 /** A type variable symbol. */
+@Immutable
 public class TyVarSymbol implements Symbol {
 
   private final Symbol owner;
@@ -41,4 +45,18 @@
   public Kind symKind() {
     return Kind.TY_PARAM;
   }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, owner);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof TyVarSymbol)) {
+      return false;
+    }
+    TyVarSymbol other = (TyVarSymbol) obj;
+    return name.equals(other.name()) && owner().equals(other.owner());
+  }
 }
diff --git a/java/com/google/turbine/bytecode/ConstantPool.java b/java/com/google/turbine/bytecode/ConstantPool.java
index 3b51c04..9279ed8 100644
--- a/java/com/google/turbine/bytecode/ConstantPool.java
+++ b/java/com/google/turbine/bytecode/ConstantPool.java
@@ -90,7 +90,9 @@
     if (classInfoPool.containsKey(utf8)) {
       return classInfoPool.get(utf8);
     }
-    return insert(new Entry(Kind.CLASS_INFO, new ShortValue(utf8)));
+    short index = insert(new Entry(Kind.CLASS_INFO, new ShortValue(utf8)));
+    classInfoPool.put(utf8, index);
+    return index;
   }
 
   /** Adds a CONSTANT_Utf8_info entry to the pool. */
@@ -98,7 +100,9 @@
     if (utf8Pool.containsKey(value)) {
       return utf8Pool.get(value);
     }
-    return insert(new Entry(Kind.UTF8, new StringValue(value)));
+    short index = insert(new Entry(Kind.UTF8, new StringValue(value)));
+    utf8Pool.put(value, index);
+    return index;
   }
 
   private short insert(Entry key) {
diff --git a/java/com/google/turbine/bytecode/LowerAttributes.java b/java/com/google/turbine/bytecode/LowerAttributes.java
index 5e71d17..75d3f42 100644
--- a/java/com/google/turbine/bytecode/LowerAttributes.java
+++ b/java/com/google/turbine/bytecode/LowerAttributes.java
@@ -28,7 +28,9 @@
   /** Collects the {@link Attribute}s for a {@link ClassFile}. */
   static List<Attribute> classAttributes(ClassFile classfile) {
     List<Attribute> attributes = new ArrayList<>();
-    attributes.add(new InnerClasses(classfile.innerClasses()));
+    if (!classfile.innerClasses().isEmpty()) {
+      attributes.add(new InnerClasses(classfile.innerClasses()));
+    }
     if (classfile.signature() != null) {
       attributes.add(new Signature(classfile.signature()));
     }
diff --git a/java/com/google/turbine/lower/Lower.java b/java/com/google/turbine/lower/Lower.java
new file mode 100644
index 0000000..cf8c7e7
--- /dev/null
+++ b/java/com/google/turbine/lower/Lower.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.turbine.lower;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.turbine.binder.bound.HeaderBoundClass;
+import com.google.turbine.binder.bound.SourceTypeBoundClass;
+import com.google.turbine.binder.bound.SourceTypeBoundClass.FieldInfo;
+import com.google.turbine.binder.bound.SourceTypeBoundClass.MethodInfo;
+import com.google.turbine.binder.bound.SourceTypeBoundClass.ParamInfo;
+import com.google.turbine.binder.bound.SourceTypeBoundClass.TyVarInfo;
+import com.google.turbine.binder.env.Env;
+import com.google.turbine.binder.lookup.Scope;
+import com.google.turbine.binder.sym.ClassSymbol;
+import com.google.turbine.binder.sym.Symbol;
+import com.google.turbine.binder.sym.TyVarSymbol;
+import com.google.turbine.bytecode.ClassFile;
+import com.google.turbine.bytecode.ClassWriter;
+import com.google.turbine.bytecode.sig.Sig;
+import com.google.turbine.bytecode.sig.Sig.MethodSig;
+import com.google.turbine.bytecode.sig.Sig.TySig;
+import com.google.turbine.bytecode.sig.SigWriter;
+import com.google.turbine.model.Const;
+import com.google.turbine.model.TurbineFlag;
+import com.google.turbine.model.TurbineVisibility;
+import com.google.turbine.type.Type;
+import com.google.turbine.type.Type.ClassTy;
+import com.google.turbine.types.Erasure;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Lowering from bound classes to bytecode. */
+public class Lower {
+
+  /** Lowers all given classes to bytecode. */
+  public static Map<String, byte[]> lowerAll(
+      Env<HeaderBoundClass> env, Iterable<ClassSymbol> syms) {
+    ImmutableMap.Builder<String, byte[]> result = ImmutableMap.builder();
+    for (ClassSymbol sym : syms) {
+      result.put(
+          sym.binaryName(), new Lower().lower((SourceTypeBoundClass) env.get(sym), env, sym));
+    }
+    return result.build();
+  }
+
+  private final LowerSignature sig = new LowerSignature();
+
+  /** Lowers a class to bytecode. */
+  public byte[] lower(SourceTypeBoundClass info, Env<HeaderBoundClass> env, ClassSymbol sym) {
+
+    int access = classAccess(info);
+    String name = sym.binaryName();
+    String signature = sig.classSignature(info);
+    String superName = info.superclass().binaryName();
+    List<String> interfaces = new ArrayList<>();
+    for (ClassSymbol i : info.interfaces()) {
+      interfaces.add(i.binaryName());
+    }
+
+    List<ClassFile.MethodInfo> methods = new ArrayList<>();
+    for (MethodInfo m : info.methods()) {
+      if (TurbineVisibility.fromAccess(m.access()) == TurbineVisibility.PRIVATE
+          && !m.name().equals("<init>")) {
+        // TODO(cushon): drop private members earlier?
+        continue;
+      }
+      methods.add(lowerMethod(env, m, sym));
+    }
+
+    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?
+        continue;
+      }
+      fields.add(lowerField(env, f));
+    }
+
+    // TODO(cushon): annotations
+    ImmutableList<ClassFile.AnnotationInfo> annotations = ImmutableList.of();
+
+    ImmutableList<ClassFile.InnerClass> inners = collectInnerClasses(sym, info, env);
+
+    ClassFile classfile =
+        new ClassFile(
+            access,
+            name,
+            signature,
+            superName,
+            interfaces,
+            methods,
+            fields.build(),
+            annotations,
+            inners);
+
+    return ClassWriter.writeClass(classfile);
+  }
+
+  private ClassFile.MethodInfo lowerMethod(
+      final Env<HeaderBoundClass> env, final MethodInfo m, final ClassSymbol sym) {
+    int access = m.access();
+    Function<TyVarSymbol, TyVarInfo> tenv = new TyVarEnv(m.tyParams(), env);
+    String name = m.name();
+    String desc = methodDescriptor(m, tenv);
+    String signature = sig.methodSignature(env, m, sym);
+    ImmutableList.Builder<String> exceptions = ImmutableList.builder();
+    if (!m.exceptions().isEmpty()) {
+      for (Type e : m.exceptions()) {
+        exceptions.add(((ClassTy) Erasure.erase(e, tenv)).sym().binaryName());
+      }
+    }
+
+    // TODO(cushon): annotations
+    ClassFile.AnnotationInfo.ElementValue defaultValue = null;
+    ImmutableList<ClassFile.AnnotationInfo> annotations = ImmutableList.of();
+    List<List<ClassFile.AnnotationInfo>> paramAnnotations = ImmutableList.of();
+
+    return new ClassFile.MethodInfo(
+        access,
+        name,
+        desc,
+        signature,
+        exceptions.build(),
+        defaultValue,
+        annotations,
+        paramAnnotations);
+  }
+
+  private String methodDescriptor(MethodInfo m, Function<TyVarSymbol, TyVarInfo> tenv) {
+    ImmutableList<Sig.TyParamSig> typarams = ImmutableList.of();
+    ImmutableList.Builder<TySig> fparams = ImmutableList.builder();
+    for (ParamInfo t : m.parameters()) {
+      fparams.add(sig.signature(Erasure.erase(t.type(), tenv)));
+    }
+    TySig result = sig.signature(Erasure.erase(m.returnType(), tenv));
+    ImmutableList<TySig> excns = ImmutableList.of();
+    return SigWriter.method(new MethodSig(typarams, fparams.build(), result, excns));
+  }
+
+  private ClassFile.FieldInfo lowerField(final Env<HeaderBoundClass> env, FieldInfo f) {
+    final String name = f.name();
+    Function<TyVarSymbol, TyVarInfo> tenv = new TyVarEnv(Collections.emptyMap(), env);
+    String desc = SigWriter.type(sig.signature(Erasure.erase(f.type(), tenv)));
+    String signature = SigWriter.type(sig.signature(f.type()));
+
+    // TODO(cushon): annotations
+    ImmutableList<ClassFile.AnnotationInfo> annotations = ImmutableList.of();
+    // TODO(cushon): constants
+    Const.Value value = null;
+
+    return new ClassFile.FieldInfo(f.access(), name, desc, signature, value, annotations);
+  }
+
+  /** Creates inner class attributes for all referenced inner classes. */
+  private ImmutableList<ClassFile.InnerClass> collectInnerClasses(
+      ClassSymbol origin, SourceTypeBoundClass info, Env<HeaderBoundClass> env) {
+    Set<ClassSymbol> all = new LinkedHashSet<>();
+    addEnclosing(env, all, origin);
+    for (ClassSymbol sym : info.children().values()) {
+      addEnclosing(env, all, sym);
+    }
+    for (ClassSymbol sym : sig.classes) {
+      addEnclosing(env, all, sym);
+    }
+    ImmutableList.Builder<ClassFile.InnerClass> inners = ImmutableList.builder();
+    for (ClassSymbol innerSym : all) {
+      inners.add(innerClass(env, innerSym));
+    }
+    return inners.build();
+  }
+
+  private void addEnclosing(Env<HeaderBoundClass> env, Set<ClassSymbol> all, ClassSymbol sym) {
+    HeaderBoundClass innerinfo = env.get(sym);
+    while (innerinfo.owner() != null) {
+      all.add(sym);
+      sym = innerinfo.owner();
+      innerinfo = env.get(sym);
+    }
+  }
+
+  /**
+   * Creates an inner class attribute, given an inner class that was referenced somewhere in the
+   * class.
+   */
+  private ClassFile.InnerClass innerClass(Env<HeaderBoundClass> env, ClassSymbol innerSym) {
+    HeaderBoundClass inner = env.get(innerSym);
+
+    String innerName = innerSym.binaryName().substring(inner.owner().binaryName().length() + 1);
+
+    int access = inner.access();
+    access &= ~TurbineFlag.ACC_SUPER;
+
+    return new ClassFile.InnerClass(
+        innerSym.binaryName(), inner.owner().binaryName(), innerName, access);
+  }
+
+  /** Updates visibility, and unsets access bits that can only be set in InnerClass. */
+  private int classAccess(SourceTypeBoundClass info) {
+    int access = info.access();
+    access &= ~(TurbineFlag.ACC_STATIC | TurbineFlag.ACC_PRIVATE);
+    if ((access & TurbineFlag.ACC_PROTECTED) != 0) {
+      access &= ~TurbineFlag.ACC_PROTECTED;
+      access |= TurbineFlag.ACC_PUBLIC;
+    }
+    return access;
+  }
+
+  /**
+   * Looks up {@link TyVarInfo}s.
+   *
+   * <p>We could generalize {@link Scope} instead, but this isn't needed anywhere else.
+   */
+  static class TyVarEnv implements Function<TyVarSymbol, TyVarInfo> {
+
+    private final Env<HeaderBoundClass> env;
+    private final Map<TyVarSymbol, TyVarInfo> tyParams;
+
+    /**
+     * @param tyParams the initial lookup scope, e.g. a method's formal type parameters.
+     * @param env the environment to look up a type variable's owning declaration in.
+     */
+    public TyVarEnv(Map<TyVarSymbol, TyVarInfo> tyParams, Env<HeaderBoundClass> env) {
+      this.tyParams = tyParams;
+      this.env = env;
+    }
+
+    @Override
+    public TyVarInfo apply(TyVarSymbol sym) {
+      TyVarInfo result = tyParams.get(sym);
+      if (result != null) {
+        return result;
+      }
+      // type variables can only be declared by methods and classes,
+      // and we've already handled methods
+      Symbol ownerSym = sym.owner();
+      if (ownerSym.symKind() != Symbol.Kind.CLASS) {
+        throw new AssertionError(sym);
+      }
+      // anything that lexically encloses the class being lowered
+      // must be in the same compilation unit, so we have source
+      // information for it
+      // TODO(cushon): remove this cast once we're reading type parameters from bytecode
+      HeaderBoundClass owner = env.get((ClassSymbol) ownerSym);
+      if (!(owner instanceof SourceTypeBoundClass)) {
+        throw new AssertionError(sym);
+      }
+      return ((SourceTypeBoundClass) owner).typeParameterTypes().get(sym);
+    }
+  }
+}
diff --git a/java/com/google/turbine/lower/LowerSignature.java b/java/com/google/turbine/lower/LowerSignature.java
index d88e3c3..bc4706f 100644
--- a/java/com/google/turbine/lower/LowerSignature.java
+++ b/java/com/google/turbine/lower/LowerSignature.java
@@ -17,9 +17,19 @@
 package com.google.turbine.lower;
 
 import com.google.common.collect.ImmutableList;
+import com.google.turbine.binder.bound.HeaderBoundClass;
+import com.google.turbine.binder.bound.SourceTypeBoundClass;
+import com.google.turbine.binder.env.Env;
+import com.google.turbine.binder.sym.ClassSymbol;
+import com.google.turbine.binder.sym.TyVarSymbol;
 import com.google.turbine.bytecode.sig.Sig;
+import com.google.turbine.bytecode.sig.Sig.ClassSig;
+import com.google.turbine.bytecode.sig.Sig.ClassTySig;
+import com.google.turbine.bytecode.sig.Sig.MethodSig;
 import com.google.turbine.bytecode.sig.Sig.SimpleClassTySig;
 import com.google.turbine.bytecode.sig.Sig.TyArgSig;
+import com.google.turbine.bytecode.sig.SigWriter;
+import com.google.turbine.model.TurbineFlag;
 import com.google.turbine.type.Type;
 import com.google.turbine.type.Type.ArrayTy;
 import com.google.turbine.type.Type.ClassTy;
@@ -29,14 +39,18 @@
 import com.google.turbine.type.Type.TyVar;
 import com.google.turbine.type.Type.WildLowerBoundedTy;
 import com.google.turbine.type.Type.WildUpperBoundedTy;
-
 import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
 
 /** Translator from {@link Type}s to {@link Sig}natures. */
 public class LowerSignature {
 
+  final Set<ClassSymbol> classes = new LinkedHashSet<>();
+
   /** Translates types to signatures. */
-  public static Sig.TySig signature(Type ty) {
+  public Sig.TySig signature(Type ty) {
     switch (ty.tyKind()) {
       case CLASS_TY:
         return classTySig((Type.ClassTy) ty);
@@ -53,19 +67,20 @@
     }
   }
 
-  private static Sig.BaseTySig refBaseTy(PrimTy t) {
+  private Sig.BaseTySig refBaseTy(PrimTy t) {
     return new Sig.BaseTySig(t.primkind());
   }
 
-  private static Sig.ArrayTySig arrayTySig(ArrayTy t) {
+  private Sig.ArrayTySig arrayTySig(ArrayTy t) {
     return new Sig.ArrayTySig(t.dimension(), signature(t.elementType()));
   }
 
-  private static Sig.TyVarSig tyVarSig(TyVar t) {
+  private Sig.TyVarSig tyVarSig(TyVar t) {
     return new Sig.TyVarSig(t.sym().name());
   }
 
-  private static Sig.ClassTySig classTySig(ClassTy t) {
+  private ClassTySig classTySig(ClassTy t) {
+    classes.add(t.sym());
     ImmutableList.Builder<SimpleClassTySig> classes = ImmutableList.builder();
     Iterator<SimpleClassTy> it = t.classes.iterator();
     SimpleClassTy curr = it.next();
@@ -89,10 +104,10 @@
       String shortname = curr.sym().binaryName().substring(outer.sym().binaryName().length() + 1);
       classes.add(new Sig.SimpleClassTySig(shortname, tyArgSigs(curr)));
     }
-    return new Sig.ClassTySig(pkg, classes.build());
+    return new ClassTySig(pkg, classes.build());
   }
 
-  private static ImmutableList<TyArgSig> tyArgSigs(SimpleClassTy part) {
+  private ImmutableList<TyArgSig> tyArgSigs(SimpleClassTy part) {
     ImmutableList.Builder<TyArgSig> tyargs = ImmutableList.builder();
     for (TyArg targ : part.targs()) {
       tyargs.add(tyArgSig(targ));
@@ -100,7 +115,7 @@
     return tyargs.build();
   }
 
-  private static TyArgSig tyArgSig(TyArg targ) {
+  private TyArgSig tyArgSig(TyArg targ) {
     switch (targ.tyArgKind()) {
       case CONCRETE:
         return new Sig.ConcreteTyArgSig(signature(((Type.ConcreteTyArg) targ).type()));
@@ -114,4 +129,157 @@
         throw new AssertionError(targ.tyArgKind());
     }
   }
+
+  /**
+   * Produces a method signature attribute for a generic method, or {@code null} if the signature is
+   * unnecessary.
+   */
+  public String methodSignature(
+      Env<HeaderBoundClass> env, SourceTypeBoundClass.MethodInfo method, ClassSymbol sym) {
+    if (!needsMethodSig(sym, env, method)) {
+      return null;
+    }
+    ImmutableList<Sig.TyParamSig> typarams = tyParamSig(method.tyParams());
+    ImmutableList.Builder<Sig.TySig> fparams = ImmutableList.builder();
+    for (SourceTypeBoundClass.ParamInfo t : method.parameters()) {
+      if (t.synthetic()) {
+        continue;
+      }
+      fparams.add(signature(t.type()));
+    }
+    Sig.TySig ret = signature(method.returnType());
+    ImmutableList.Builder<Sig.TySig> excn = ImmutableList.builder();
+    boolean needsExnSig = false;
+    for (Type e : method.exceptions()) {
+      if (needsSig(e)) {
+        needsExnSig = true;
+        break;
+      }
+    }
+    if (needsExnSig) {
+      for (Type e : method.exceptions()) {
+        excn.add(signature(e));
+      }
+    }
+    MethodSig sig = new MethodSig(typarams, fparams.build(), ret, excn.build());
+    return SigWriter.method(sig);
+  }
+
+  private boolean needsMethodSig(
+      ClassSymbol sym, Env<HeaderBoundClass> env, SourceTypeBoundClass.MethodInfo m) {
+    if ((env.get(sym).access() & TurbineFlag.ACC_ENUM) == TurbineFlag.ACC_ENUM
+        && m.name().equals("<init>")) {
+      // JDK-8024694: javac always expects signature attribute for enum constructors
+      return true;
+    }
+    if ((m.access() & TurbineFlag.ACC_SYNTH_CTOR) == TurbineFlag.ACC_SYNTH_CTOR) {
+      return false;
+    }
+    if (!m.tyParams().isEmpty()) {
+      return true;
+    }
+    if (m.returnType() != null && needsSig(m.returnType())) {
+      return true;
+    }
+    for (SourceTypeBoundClass.ParamInfo t : m.parameters()) {
+      if (t.synthetic()) {
+        continue;
+      }
+      if (needsSig(t.type())) {
+        return true;
+      }
+    }
+    for (Type t : m.exceptions()) {
+      if (needsSig(t)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Produces a class signature attribute for a generic class, or {@code null} if the signature is
+   * unnecessary.
+   */
+  public String classSignature(SourceTypeBoundClass info) {
+    if (!classNeedsSig(info)) {
+      return null;
+    }
+    ImmutableList<Sig.TyParamSig> typarams = tyParamSig(info.typeParameterTypes());
+
+    ClassTySig xtnd = null;
+    if (info.superClassType() != null) {
+      xtnd = classTySig(info.superClassType());
+    }
+    ImmutableList.Builder<ClassTySig> impl = ImmutableList.builder();
+    for (ClassTy i : info.interfaceTypes()) {
+      impl.add(classTySig(i));
+    }
+    ClassSig sig = new ClassSig(typarams, xtnd, impl.build());
+    return SigWriter.classSig(sig);
+  }
+
+  private boolean classNeedsSig(SourceTypeBoundClass ci) {
+    if (!ci.typeParameters().isEmpty()) {
+      return true;
+    }
+    if (ci.superClassType() != null && needsSig(ci.superClassType())) {
+      return true;
+    }
+    for (ClassTy i : ci.interfaceTypes()) {
+      if (needsSig(i)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean needsSig(Type ty) {
+    switch (ty.tyKind()) {
+      case PRIM_TY:
+      case VOID_TY:
+        return false;
+      case CLASS_TY:
+        {
+          for (SimpleClassTy s : ((ClassTy) ty).classes) {
+            if (!s.targs().isEmpty()) {
+              return true;
+            }
+          }
+          return false;
+        }
+      case ARRAY_TY:
+        return needsSig(((ArrayTy) ty).elementType());
+      case TY_VAR:
+        return true;
+      default:
+        throw new AssertionError(ty.tyKind());
+    }
+  }
+
+  private ImmutableList<Sig.TyParamSig> tyParamSig(
+      Map<TyVarSymbol, SourceTypeBoundClass.TyVarInfo> px) {
+    ImmutableList.Builder<Sig.TyParamSig> result = ImmutableList.builder();
+    for (Map.Entry<TyVarSymbol, SourceTypeBoundClass.TyVarInfo> entry : px.entrySet()) {
+      result.add(tyParamSig(entry.getKey(), entry.getValue()));
+    }
+    return result.build();
+  }
+
+  private Sig.TyParamSig tyParamSig(TyVarSymbol sym, SourceTypeBoundClass.TyVarInfo info) {
+    String identifier = sym.name();
+    Sig.TySig cbound = null;
+    if (info.superClassBound() != null) {
+      cbound = signature(info.superClassBound());
+    } else if (info.interfaceBounds().isEmpty()) {
+      cbound =
+          new ClassTySig(
+              "java/lang", ImmutableList.of(new SimpleClassTySig("Object", ImmutableList.of())));
+    }
+    ImmutableList.Builder<Sig.TySig> ibounds = ImmutableList.builder();
+    for (Type i : info.interfaceBounds()) {
+      ibounds.add(signature(i));
+    }
+    return new Sig.TyParamSig(identifier, cbound, ibounds.build());
+  }
 }
diff --git a/java/com/google/turbine/type/Type.java b/java/com/google/turbine/type/Type.java
index 678c27b..004f974 100644
--- a/java/com/google/turbine/type/Type.java
+++ b/java/com/google/turbine/type/Type.java
@@ -22,7 +22,6 @@
 import com.google.turbine.binder.sym.ClassSymbol;
 import com.google.turbine.binder.sym.TyVarSymbol;
 import com.google.turbine.model.TurbineConstantTypeKind;
-
 import java.util.Arrays;
 
 /** JLS 4 types. */
@@ -67,6 +66,9 @@
      */
     public static final ClassTy OBJECT = asNonParametricClassTy(ClassSymbol.OBJECT);
 
+    /** The {@link ClassTy} for {@code java.lang.String}. */
+    public static final ClassTy STRING = asNonParametricClassTy(ClassSymbol.STRING);
+
     /** Returns a {@link ClassTy} with no type arguments for the given {@link ClassSymbol}. */
     public static ClassTy asNonParametricClassTy(ClassSymbol i) {
       return new ClassTy(Arrays.asList(new SimpleClassTy(i, ImmutableList.of())));
diff --git a/java/com/google/turbine/types/Erasure.java b/java/com/google/turbine/types/Erasure.java
new file mode 100644
index 0000000..80762be
--- /dev/null
+++ b/java/com/google/turbine/types/Erasure.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.turbine.types;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.turbine.binder.bound.SourceTypeBoundClass;
+import com.google.turbine.binder.sym.TyVarSymbol;
+import com.google.turbine.type.Type;
+import com.google.turbine.type.Type.TyVar;
+
+/** Generic type erasure. */
+public class Erasure {
+  public static Type erase(Type ty, Function<TyVarSymbol, SourceTypeBoundClass.TyVarInfo> tenv) {
+    switch (ty.tyKind()) {
+      case PRIM_TY:
+      case VOID_TY:
+        return ty;
+      case CLASS_TY:
+        return eraseClassTy((Type.ClassTy) ty);
+      case ARRAY_TY:
+        return eraseArrayTy((Type.ArrayTy) ty, tenv);
+      case TY_VAR:
+        return eraseTyVar((TyVar) ty, tenv);
+      default:
+        throw new AssertionError(ty.tyKind());
+    }
+  }
+
+  private static Type eraseTyVar(
+      TyVar ty, Function<TyVarSymbol, SourceTypeBoundClass.TyVarInfo> tenv) {
+    SourceTypeBoundClass.TyVarInfo info = tenv.apply(ty.sym());
+    if (info.superClassBound() != null) {
+      return erase(info.superClassBound(), tenv);
+    }
+    if (!info.interfaceBounds().isEmpty()) {
+      return erase(info.interfaceBounds().get(0), tenv);
+    }
+    return Type.ClassTy.OBJECT;
+  }
+
+  private static Type eraseArrayTy(
+      Type.ArrayTy ty, Function<TyVarSymbol, SourceTypeBoundClass.TyVarInfo> tenv) {
+    return new Type.ArrayTy(ty.dimension(), erase(ty.elementType(), tenv));
+  }
+
+  private static Type eraseClassTy(Type.ClassTy ty) {
+    ImmutableList.Builder<Type.ClassTy.SimpleClassTy> classes = ImmutableList.builder();
+    for (Type.ClassTy.SimpleClassTy c : ty.classes) {
+      if (c.targs().isEmpty()) {
+        classes.add(c);
+      } else {
+        classes.add(new Type.ClassTy.SimpleClassTy(c.sym(), ImmutableList.<Type.TyArg>of()));
+      }
+    }
+    return new Type.ClassTy(classes.build());
+  }
+}
diff --git a/javatests/com/google/turbine/bytecode/AsmUtils.java b/javatests/com/google/turbine/bytecode/AsmUtils.java
index 955f88f..6030e16 100644
--- a/javatests/com/google/turbine/bytecode/AsmUtils.java
+++ b/javatests/com/google/turbine/bytecode/AsmUtils.java
@@ -27,7 +27,7 @@
  * com.google.turbine.bytecode.ClassReader}.
  */
 public class AsmUtils {
-  static String textify(byte[] bytes) {
+  public static String textify(byte[] bytes) {
     Textifier textifier = new Textifier();
     StringWriter sw = new StringWriter();
     new ClassReader(bytes)
diff --git a/javatests/com/google/turbine/lower/LowerSignatureTest.java b/javatests/com/google/turbine/lower/LowerSignatureTest.java
index bad07bf..cfd57cf 100644
--- a/javatests/com/google/turbine/lower/LowerSignatureTest.java
+++ b/javatests/com/google/turbine/lower/LowerSignatureTest.java
@@ -37,20 +37,21 @@
             ImmutableList.of(
                 new Type.ClassTy.SimpleClassTy(
                     new ClassSymbol("java/util/List"), ImmutableList.of())));
-    assertThat(SigWriter.type(LowerSignature.signature(type))).isEqualTo("Ljava/util/List;");
+    assertThat(SigWriter.type(new LowerSignature().signature(type))).isEqualTo("Ljava/util/List;");
   }
 
   @Test
   public void inner() {
     assertThat(
             SigWriter.type(
-                LowerSignature.signature(
-                    new Type.ClassTy(
-                        ImmutableList.of(
-                            new Type.ClassTy.SimpleClassTy(
-                                new ClassSymbol("test/Outer"), ImmutableList.of()),
-                            new Type.ClassTy.SimpleClassTy(
-                                new ClassSymbol("test/Outer$Inner"), ImmutableList.of()))))))
+                new LowerSignature()
+                    .signature(
+                        new Type.ClassTy(
+                            ImmutableList.of(
+                                new Type.ClassTy.SimpleClassTy(
+                                    new ClassSymbol("test/Outer"), ImmutableList.of()),
+                                new Type.ClassTy.SimpleClassTy(
+                                    new ClassSymbol("test/Outer$Inner"), ImmutableList.of()))))))
         .isEqualTo("Ltest/Outer$Inner;");
   }
 
@@ -65,7 +66,7 @@
                 new Type.ClassTy.SimpleClassTy(
                     new ClassSymbol("test/Outer$Inner"),
                     ImmutableList.of(new Type.ConcreteTyArg(Type.ClassTy.OBJECT)))));
-    assertThat(SigWriter.type(LowerSignature.signature(type)))
+    assertThat(SigWriter.type(new LowerSignature().signature(type)))
         .isEqualTo("Ltest/Outer<Ljava/lang/Object;>.Inner<Ljava/lang/Object;>;");
     // Type#toString is only for debugging
     assertThat(type.toString()).isEqualTo("test/Outer<java/lang/Object>.Inner<java/lang/Object>");
@@ -75,13 +76,14 @@
   public void innerDefaultPackage() {
     assertThat(
             SigWriter.type(
-                LowerSignature.signature(
-                    new Type.ClassTy(
-                        ImmutableList.of(
-                            new Type.ClassTy.SimpleClassTy(
-                                new ClassSymbol("Outer"), ImmutableList.of()),
-                            new Type.ClassTy.SimpleClassTy(
-                                new ClassSymbol("Outer$Inner"), ImmutableList.of()))))))
+                new LowerSignature()
+                    .signature(
+                        new Type.ClassTy(
+                            ImmutableList.of(
+                                new Type.ClassTy.SimpleClassTy(
+                                    new ClassSymbol("Outer"), ImmutableList.of()),
+                                new Type.ClassTy.SimpleClassTy(
+                                    new ClassSymbol("Outer$Inner"), ImmutableList.of()))))))
         .isEqualTo("LOuter$Inner;");
   }
 
@@ -89,15 +91,16 @@
   public void wildcard() {
     assertThat(
             SigWriter.type(
-                LowerSignature.signature(
-                    new Type.ClassTy(
-                        ImmutableList.of(
-                            new Type.ClassTy.SimpleClassTy(
-                                new ClassSymbol("test/Test"),
-                                ImmutableList.of(
-                                    new Type.WildTy(),
-                                    new Type.WildLowerBoundedTy(Type.ClassTy.OBJECT),
-                                    new Type.WildUpperBoundedTy(Type.ClassTy.OBJECT))))))))
+                new LowerSignature()
+                    .signature(
+                        new Type.ClassTy(
+                            ImmutableList.of(
+                                new Type.ClassTy.SimpleClassTy(
+                                    new ClassSymbol("test/Test"),
+                                    ImmutableList.of(
+                                        new Type.WildTy(),
+                                        new Type.WildLowerBoundedTy(Type.ClassTy.OBJECT),
+                                        new Type.WildUpperBoundedTy(Type.ClassTy.OBJECT))))))))
         .isEqualTo("Ltest/Test<*-Ljava/lang/Object;+Ljava/lang/Object;>;");
   }
 
@@ -105,7 +108,8 @@
   public void tyVar() {
     assertThat(
             SigWriter.type(
-                LowerSignature.signature(new Type.TyVar(new TyVarSymbol(ClassSymbol.OBJECT, "X")))))
+                new LowerSignature()
+                    .signature(new Type.TyVar(new TyVarSymbol(ClassSymbol.OBJECT, "X")))))
         .isEqualTo("TX;");
   }
 
@@ -113,21 +117,22 @@
   public void primitive() {
     assertThat(
             SigWriter.type(
-                LowerSignature.signature(new Type.PrimTy(TurbineConstantTypeKind.BOOLEAN))))
+                new LowerSignature().signature(new Type.PrimTy(TurbineConstantTypeKind.BOOLEAN))))
         .isEqualTo("Z");
   }
 
   @Test
   public void voidType() {
-    assertThat(SigWriter.type(LowerSignature.signature(Type.VOID))).isEqualTo("V");
+    assertThat(SigWriter.type(new LowerSignature().signature(Type.VOID))).isEqualTo("V");
   }
 
   @Test
   public void array() {
     assertThat(
             SigWriter.type(
-                LowerSignature.signature(
-                    new Type.ArrayTy(3, new Type.PrimTy(TurbineConstantTypeKind.BOOLEAN)))))
+                new LowerSignature()
+                    .signature(
+                        new Type.ArrayTy(3, new Type.PrimTy(TurbineConstantTypeKind.BOOLEAN)))))
         .isEqualTo("[[[Z");
   }
 }
diff --git a/javatests/com/google/turbine/lower/LowerTest.java b/javatests/com/google/turbine/lower/LowerTest.java
new file mode 100644
index 0000000..c88883e
--- /dev/null
+++ b/javatests/com/google/turbine/lower/LowerTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.turbine.lower;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.ByteStreams;
+import com.google.turbine.binder.ClassPathBinder;
+import com.google.turbine.binder.bound.HeaderBoundClass;
+import com.google.turbine.binder.bound.SourceTypeBoundClass;
+import com.google.turbine.binder.bytecode.BytecodeBoundClass;
+import com.google.turbine.binder.env.CompoundEnv;
+import com.google.turbine.binder.env.SimpleEnv;
+import com.google.turbine.binder.lookup.TopLevelIndex;
+import com.google.turbine.binder.sym.ClassSymbol;
+import com.google.turbine.binder.sym.FieldSymbol;
+import com.google.turbine.binder.sym.MethodSymbol;
+import com.google.turbine.binder.sym.TyVarSymbol;
+import com.google.turbine.bytecode.AsmUtils;
+import com.google.turbine.model.TurbineConstantTypeKind;
+import com.google.turbine.model.TurbineFlag;
+import com.google.turbine.model.TurbineTyKind;
+import com.google.turbine.type.Type;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class LowerTest {
+
+  private static final ImmutableList<Path> BOOTCLASSPATH =
+      ImmutableList.of(Paths.get(System.getProperty("java.home")).resolve("lib/rt.jar"));
+
+  @Test
+  public void hello() throws Exception {
+    CompoundEnv<BytecodeBoundClass> classpath =
+        ClassPathBinder.bind(ImmutableList.of(), BOOTCLASSPATH, TopLevelIndex.builder());
+
+    ImmutableList<Type.ClassTy> interfaceTypes =
+        ImmutableList.of(
+            new Type.ClassTy(
+                ImmutableList.of(
+                    new Type.ClassTy.SimpleClassTy(
+                        new ClassSymbol("java/util/List"),
+                        ImmutableList.of(
+                            new Type.ConcreteTyArg(
+                                new Type.TyVar(
+                                    new TyVarSymbol(new ClassSymbol("test/Test"), "V"))))))));
+    Type.ClassTy xtnds = Type.ClassTy.OBJECT;
+    ImmutableMap<TyVarSymbol, SourceTypeBoundClass.TyVarInfo> tps =
+        ImmutableMap.of(
+            new TyVarSymbol(new ClassSymbol("test/Test"), "V"),
+            new SourceTypeBoundClass.TyVarInfo(
+                new Type.ClassTy(
+                    ImmutableList.of(
+                        new Type.ClassTy.SimpleClassTy(
+                            new ClassSymbol("test/Test$Inner"), ImmutableList.of()))),
+                ImmutableList.of()));
+    int access = TurbineFlag.ACC_SUPER | TurbineFlag.ACC_PUBLIC;
+    ImmutableList<SourceTypeBoundClass.MethodInfo> methods =
+        ImmutableList.of(
+            new SourceTypeBoundClass.MethodInfo(
+                new MethodSymbol(new ClassSymbol("test/Test"), "f"),
+                ImmutableMap.of(),
+                new Type.PrimTy(TurbineConstantTypeKind.INT),
+                ImmutableList.of(),
+                ImmutableList.of(),
+                TurbineFlag.ACC_STATIC | TurbineFlag.ACC_PUBLIC),
+            new SourceTypeBoundClass.MethodInfo(
+                new MethodSymbol(new ClassSymbol("test/Test"), "g"),
+                ImmutableMap.of(
+                    new TyVarSymbol(new MethodSymbol(new ClassSymbol("test/Test"), "g"), "V"),
+                    new SourceTypeBoundClass.TyVarInfo(
+                        null,
+                        ImmutableList.of(
+                            new Type.ClassTy(
+                                ImmutableList.of(
+                                    new Type.ClassTy.SimpleClassTy(
+                                        new ClassSymbol("java/lang/Runnable"),
+                                        ImmutableList.of()))))),
+                    new TyVarSymbol(new MethodSymbol(new ClassSymbol("test/Test"), "g"), "E"),
+                    new SourceTypeBoundClass.TyVarInfo(
+                        new Type.ClassTy(
+                            ImmutableList.of(
+                                new Type.ClassTy.SimpleClassTy(
+                                    new ClassSymbol("java/lang/Error"), ImmutableList.of()))),
+                        ImmutableList.of())),
+                Type.VOID,
+                ImmutableList.of(
+                    new SourceTypeBoundClass.ParamInfo(
+                        new Type.PrimTy(TurbineConstantTypeKind.INT), false)),
+                ImmutableList.of(
+                    new Type.TyVar(
+                        new TyVarSymbol(new MethodSymbol(new ClassSymbol("test/Test"), "g"), "E"))),
+                TurbineFlag.ACC_PUBLIC));
+    ImmutableList<SourceTypeBoundClass.FieldInfo> fields =
+        ImmutableList.of(
+            new SourceTypeBoundClass.FieldInfo(
+                new FieldSymbol(new ClassSymbol("test/Test"), "theField"),
+                Type.ClassTy.asNonParametricClassTy(new ClassSymbol("test/Test$Inner")),
+                TurbineFlag.ACC_STATIC | TurbineFlag.ACC_FINAL | TurbineFlag.ACC_PUBLIC));
+    ClassSymbol owner = null;
+    TurbineTyKind kind = TurbineTyKind.CLASS;
+    ImmutableMap<String, ClassSymbol> children = ImmutableMap.of();
+    ClassSymbol superclass = ClassSymbol.OBJECT;
+    ImmutableList<ClassSymbol> interfaces = ImmutableList.of(new ClassSymbol("java/util/List"));
+    ImmutableMap<String, TyVarSymbol> tyParams =
+        ImmutableMap.of("V", new TyVarSymbol(new ClassSymbol("test/Test"), "V"));
+
+    SourceTypeBoundClass c =
+        new SourceTypeBoundClass(
+            interfaceTypes,
+            xtnds,
+            tps,
+            access,
+            methods,
+            fields,
+            owner,
+            kind,
+            children,
+            superclass,
+            interfaces,
+            tyParams);
+
+    SourceTypeBoundClass i =
+        new SourceTypeBoundClass(
+            ImmutableList.of(),
+            Type.ClassTy.OBJECT,
+            ImmutableMap.of(),
+            TurbineFlag.ACC_STATIC | TurbineFlag.ACC_PROTECTED,
+            ImmutableList.of(),
+            ImmutableList.of(),
+            new ClassSymbol("test/Test"),
+            TurbineTyKind.CLASS,
+            ImmutableMap.of("Inner", new ClassSymbol("test/Test$Inner")),
+            ClassSymbol.OBJECT,
+            ImmutableList.of(),
+            ImmutableMap.of());
+
+    SimpleEnv.Builder<SourceTypeBoundClass> b = SimpleEnv.builder();
+    b.putIfAbsent(new ClassSymbol("test/Test"), c);
+    b.putIfAbsent(new ClassSymbol("test/Test$Inner"), i);
+    SimpleEnv<SourceTypeBoundClass> env = b.build();
+
+    Map<String, byte[]> bytes =
+        Lower.lowerAll(
+            CompoundEnv.<HeaderBoundClass>of(classpath).append(env),
+            ImmutableList.of(new ClassSymbol("test/Test"), new ClassSymbol("test/Test$Inner")));
+
+    assertThat(AsmUtils.textify(bytes.get("test/Test")))
+        .isEqualTo(
+            new String(
+                ByteStreams.toByteArray(
+                    LowerTest.class.getResourceAsStream("testdata/golden/outer.txt")),
+                UTF_8));
+    assertThat(AsmUtils.textify(bytes.get("test/Test$Inner")))
+        .isEqualTo(
+            new String(
+                ByteStreams.toByteArray(
+                    LowerTest.class.getResourceAsStream("testdata/golden/inner.txt")),
+                UTF_8));
+  }
+}
diff --git a/javatests/com/google/turbine/lower/testdata/golden/inner.txt b/javatests/com/google/turbine/lower/testdata/golden/inner.txt
new file mode 100644
index 0000000..6ce4974
--- /dev/null
+++ b/javatests/com/google/turbine/lower/testdata/golden/inner.txt
@@ -0,0 +1,7 @@
+// class version 52.0 (52)
+// access flags 0x1
+public class test/Test$Inner {
+
+  // access flags 0xC
+  protected static INNERCLASS test/Test$Inner test/Test Inner
+}
diff --git a/javatests/com/google/turbine/lower/testdata/golden/outer.txt b/javatests/com/google/turbine/lower/testdata/golden/outer.txt
new file mode 100644
index 0000000..083b030
--- /dev/null
+++ b/javatests/com/google/turbine/lower/testdata/golden/outer.txt
@@ -0,0 +1,22 @@
+// class version 52.0 (52)
+// access flags 0x21
+// signature <V:Ltest/Test$Inner;>Ljava/lang/Object;Ljava/util/List<TV;>;
+// declaration: test/Test<V extends test.Test$Inner> implements java.util.List<V>
+public class test/Test implements java/util/List  {
+
+  // access flags 0xC
+  protected static INNERCLASS test/Test$Inner test/Test Inner
+
+  // access flags 0x19
+  // signature Ltest/Test$Inner;
+  // declaration: test.Test$Inner
+  public final static Ltest/Test$Inner; theField
+
+  // access flags 0x9
+  public static f()I
+
+  // access flags 0x1
+  // signature <V::Ljava/lang/Runnable;E:Ljava/lang/Error;>(I)V^TE;
+  // declaration: void g<V extends java.lang.Runnable, E extends java.lang.Error>(int) throws E
+  public g(I)V throws java/lang/Error 
+}