Class writing support for module attributes

MOE_MIGRATED_REVID=182568594
diff --git a/java/com/google/turbine/bytecode/Attribute.java b/java/com/google/turbine/bytecode/Attribute.java
index 0700744..29efb60 100644
--- a/java/com/google/turbine/bytecode/Attribute.java
+++ b/java/com/google/turbine/bytecode/Attribute.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo;
 import com.google.turbine.bytecode.ClassFile.MethodInfo.ParameterInfo;
+import com.google.turbine.bytecode.ClassFile.ModuleInfo;
 import com.google.turbine.bytecode.ClassFile.TypeAnnotationInfo;
 import com.google.turbine.model.Const.Value;
 import java.util.List;
@@ -39,7 +40,8 @@
     DEPRECATED("Deprecated"),
     RUNTIME_VISIBLE_TYPE_ANNOTATIONS("RuntimeVisibleTypeAnnotations"),
     RUNTIME_INVISIBLE_TYPE_ANNOTATIONS("RuntimeInvisibleTypeAnnotations"),
-    METHOD_PARAMETERS("MethodParameters");
+    METHOD_PARAMETERS("MethodParameters"),
+    MODULE("Module");
 
     private final String signature;
 
@@ -288,4 +290,23 @@
       return Kind.METHOD_PARAMETERS;
     }
   }
+
+  /** A JVMS §4.7.25 Module attribute. */
+  class Module implements Attribute {
+
+    private final ModuleInfo module;
+
+    public Module(ModuleInfo module) {
+      this.module = module;
+    }
+
+    @Override
+    public Kind kind() {
+      return Kind.MODULE;
+    }
+
+    public ModuleInfo module() {
+      return module;
+    }
+  }
 }
diff --git a/java/com/google/turbine/bytecode/AttributeWriter.java b/java/com/google/turbine/bytecode/AttributeWriter.java
index 4eece56..8179bc4 100644
--- a/java/com/google/turbine/bytecode/AttributeWriter.java
+++ b/java/com/google/turbine/bytecode/AttributeWriter.java
@@ -27,6 +27,12 @@
 import com.google.turbine.bytecode.Attribute.TypeAnnotations;
 import com.google.turbine.bytecode.ClassFile.AnnotationInfo;
 import com.google.turbine.bytecode.ClassFile.MethodInfo.ParameterInfo;
+import com.google.turbine.bytecode.ClassFile.ModuleInfo;
+import com.google.turbine.bytecode.ClassFile.ModuleInfo.ExportInfo;
+import com.google.turbine.bytecode.ClassFile.ModuleInfo.OpenInfo;
+import com.google.turbine.bytecode.ClassFile.ModuleInfo.ProvideInfo;
+import com.google.turbine.bytecode.ClassFile.ModuleInfo.RequireInfo;
+import com.google.turbine.bytecode.ClassFile.ModuleInfo.UseInfo;
 import com.google.turbine.bytecode.ClassFile.TypeAnnotationInfo;
 import com.google.turbine.model.Const;
 import java.util.List;
@@ -78,6 +84,9 @@
       case METHOD_PARAMETERS:
         writeMethodParameters((Attribute.MethodParameters) attribute);
         break;
+      case MODULE:
+        writeModule((Attribute.Module) attribute);
+        break;
       default:
         throw new AssertionError(attribute.kind());
     }
@@ -203,4 +212,60 @@
       output.writeShort(parameter.access());
     }
   }
+
+  private void writeModule(Attribute.Module attribute) {
+    ModuleInfo module = attribute.module();
+
+    ByteArrayDataOutput tmp = ByteStreams.newDataOutput();
+
+    tmp.writeShort(pool.moduleInfo(module.name()));
+    tmp.writeShort(module.flags());
+    tmp.writeShort(pool.utf8(module.version()));
+
+    tmp.writeShort(module.requires().size());
+    for (RequireInfo require : module.requires()) {
+      tmp.writeShort(pool.moduleInfo(require.moduleName()));
+      tmp.writeShort(require.flags());
+      tmp.writeShort(pool.utf8(require.version()));
+    }
+
+    tmp.writeShort(module.exports().size());
+    for (ExportInfo export : module.exports()) {
+      tmp.writeShort(pool.packageInfo(export.moduleName()));
+      tmp.writeShort(export.flags());
+      tmp.writeShort(export.modules().size());
+      for (String exportedModule : export.modules()) {
+        tmp.writeShort(pool.moduleInfo(exportedModule));
+      }
+    }
+
+    tmp.writeShort(module.opens().size());
+    for (OpenInfo opens : module.opens()) {
+      tmp.writeShort(pool.packageInfo(opens.moduleName()));
+      tmp.writeShort(opens.flags());
+      tmp.writeShort(opens.modules().size());
+      for (String openModule : opens.modules()) {
+        tmp.writeShort(pool.moduleInfo(openModule));
+      }
+    }
+
+    tmp.writeShort(module.uses().size());
+    for (UseInfo use : module.uses()) {
+      tmp.writeShort(pool.classInfo(use.descriptor()));
+    }
+
+    tmp.writeShort(module.provides().size());
+    for (ProvideInfo provide : module.provides()) {
+      tmp.writeShort(pool.classInfo(provide.descriptor()));
+      tmp.writeShort(provide.implDescriptors().size());
+      for (String impl : provide.implDescriptors()) {
+        tmp.writeShort(pool.classInfo(impl));
+      }
+    }
+
+    byte[] data = tmp.toByteArray();
+    output.writeShort(pool.utf8(attribute.kind().signature()));
+    output.writeInt(data.length);
+    output.write(data);
+  }
 }
diff --git a/java/com/google/turbine/bytecode/ClassWriter.java b/java/com/google/turbine/bytecode/ClassWriter.java
index 42aff6c..4a89ec8 100644
--- a/java/com/google/turbine/bytecode/ClassWriter.java
+++ b/java/com/google/turbine/bytecode/ClassWriter.java
@@ -31,8 +31,10 @@
 
   private static final int MAGIC = 0xcafebabe;
   private static final int MINOR_VERSION = 0;
-  // TODO(cushon): configuration?
+  // use the lowest classfile version possible given the class file features
+  // TODO(cushon): is there a reason to support --release?
   private static final int MAJOR_VERSION = 52;
+  private static final int MODULE_MAJOR_VERSION = 53;
 
   /** Writes a {@link ClassFile} to bytecode. */
   public static byte[] writeClass(ClassFile classfile) {
@@ -54,7 +56,7 @@
       writeMethod(pool, output, m);
     }
     writeAttributes(pool, output, LowerAttributes.classAttributes(classfile));
-    return finishClass(pool, output);
+    return finishClass(pool, output, classfile);
   }
 
   private static void writeMethod(
@@ -89,6 +91,8 @@
       switch (e.kind()) {
         case CLASS_INFO:
         case STRING:
+        case MODULE:
+        case PACKAGE:
           output.writeShort(((IntValue) value).value());
           break;
         case INTEGER:
@@ -112,11 +116,12 @@
     }
   }
 
-  private static byte[] finishClass(ConstantPool pool, ByteArrayDataOutput body) {
+  private static byte[] finishClass(
+      ConstantPool pool, ByteArrayDataOutput body, ClassFile classfile) {
     ByteArrayDataOutput result = ByteStreams.newDataOutput();
     result.writeInt(MAGIC);
     result.writeShort(MINOR_VERSION);
-    result.writeShort(MAJOR_VERSION);
+    result.writeShort(classfile.module() != null ? MODULE_MAJOR_VERSION : MAJOR_VERSION);
     writeConstantPool(pool, result);
     result.write(body.toByteArray());
     return result.toByteArray();
diff --git a/java/com/google/turbine/bytecode/ConstantPool.java b/java/com/google/turbine/bytecode/ConstantPool.java
index 2f3141a..b423cfc 100644
--- a/java/com/google/turbine/bytecode/ConstantPool.java
+++ b/java/com/google/turbine/bytecode/ConstantPool.java
@@ -40,6 +40,8 @@
   private final Map<Double, Integer> doublePool = new HashMap<>();
   private final Map<Float, Integer> floatPool = new HashMap<>();
   private final Map<Long, Integer> longPool = new HashMap<>();
+  private final Map<Integer, Integer> modulePool = new HashMap<>();
+  private final Map<Integer, Integer> packagePool = new HashMap<>();
 
   private final List<Entry> constants = new ArrayList<>();
 
@@ -56,6 +58,8 @@
       case INTEGER:
       case UTF8:
       case FLOAT:
+      case MODULE:
+      case PACKAGE:
         return 1;
       case LONG:
       case DOUBLE:
@@ -158,6 +162,30 @@
     return index;
   }
 
+  /** Adds a CONSTANT_Module_info entry to the pool. */
+  int moduleInfo(String value) {
+    Objects.requireNonNull(value);
+    int utf8 = utf8(value);
+    if (modulePool.containsKey(utf8)) {
+      return modulePool.get(utf8);
+    }
+    int index = insert(new Entry(Kind.MODULE, new IntValue(utf8)));
+    modulePool.put(utf8, index);
+    return index;
+  }
+
+  /** Adds a CONSTANT_Package_info entry to the pool. */
+  int packageInfo(String value) {
+    Objects.requireNonNull(value);
+    int utf8 = utf8(value);
+    if (packagePool.containsKey(utf8)) {
+      return packagePool.get(utf8);
+    }
+    int index = insert(new Entry(Kind.PACKAGE, new IntValue(utf8)));
+    packagePool.put(utf8, index);
+    return index;
+  }
+
   private int insert(Entry key) {
     int entry = nextEntry;
     constants.add(key);
@@ -176,7 +204,9 @@
     DOUBLE(6),
     FLOAT(4),
     LONG(5),
-    UTF8(1);
+    UTF8(1),
+    MODULE(19),
+    PACKAGE(20);
 
     private final short tag;
 
diff --git a/java/com/google/turbine/bytecode/LowerAttributes.java b/java/com/google/turbine/bytecode/LowerAttributes.java
index 1752456..67ef2b4 100644
--- a/java/com/google/turbine/bytecode/LowerAttributes.java
+++ b/java/com/google/turbine/bytecode/LowerAttributes.java
@@ -42,6 +42,9 @@
     if (classfile.signature() != null) {
       attributes.add(new Signature(classfile.signature()));
     }
+    if (classfile.module() != null) {
+      attributes.add(new Attribute.Module(classfile.module()));
+    }
     return attributes;
   }
 
diff --git a/javatests/com/google/turbine/bytecode/ClassWriterTest.java b/javatests/com/google/turbine/bytecode/ClassWriterTest.java
index 2247812..e544c15 100644
--- a/javatests/com/google/turbine/bytecode/ClassWriterTest.java
+++ b/javatests/com/google/turbine/bytecode/ClassWriterTest.java
@@ -24,6 +24,7 @@
 import com.google.common.io.ByteStreams;
 import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
+import com.google.turbine.testing.AsmUtils;
 import com.sun.source.util.JavacTask;
 import com.sun.tools.javac.api.JavacTool;
 import com.sun.tools.javac.file.JavacFileManager;
@@ -42,6 +43,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.objectweb.asm.ModuleVisitor;
+import org.objectweb.asm.Opcodes;
 
 @RunWith(JUnit4.class)
 public class ClassWriterTest {
@@ -108,4 +111,43 @@
       assertThat(reader.classInfo(entry.getKey())).isEqualTo(entry.getValue());
     }
   }
+
+  @Test
+  public void module() throws Exception {
+
+    org.objectweb.asm.ClassWriter cw = new org.objectweb.asm.ClassWriter(0);
+
+    cw.visit(53, /* access= */ 53, "module-info", null, null, null);
+
+    ModuleVisitor mv = cw.visitModule("mod", Opcodes.ACC_OPEN, "mod-ver");
+
+    mv.visitRequire("r1", Opcodes.ACC_TRANSITIVE, "r1-ver");
+    mv.visitRequire("r2", Opcodes.ACC_STATIC_PHASE, "r2-ver");
+    mv.visitRequire("r3", Opcodes.ACC_STATIC_PHASE | Opcodes.ACC_TRANSITIVE, "r3-ver");
+
+    mv.visitExport("e1", Opcodes.ACC_SYNTHETIC, "e1m1", "e1m2", "e1m3");
+    mv.visitExport("e2", Opcodes.ACC_MANDATED, "e2m1", "e2m2");
+    mv.visitExport("e3", /* access= */ 0, "e3m1");
+
+    mv.visitOpen("o1", Opcodes.ACC_SYNTHETIC, "o1m1", "o1m2", "o1m3");
+    mv.visitOpen("o2", Opcodes.ACC_MANDATED, "o2m1", "o2m2");
+    mv.visitOpen("o3", /* access= */ 0, "o3m1");
+
+    mv.visitUse("u1");
+    mv.visitUse("u2");
+    mv.visitUse("u3");
+    mv.visitUse("u4");
+
+    mv.visitProvide("p1", "p1i1", "p1i2");
+    mv.visitProvide("p2", "p2i1", "p2i2", "p2i3");
+
+    byte[] inputBytes = cw.toByteArray();
+    byte[] outputBytes = ClassWriter.writeClass(ClassReader.read("module-info", inputBytes));
+
+    assertThat(AsmUtils.textify(inputBytes)).isEqualTo(AsmUtils.textify(outputBytes));
+
+    // test a round trip
+    outputBytes = ClassWriter.writeClass(ClassReader.read("module-info", outputBytes));
+    assertThat(AsmUtils.textify(inputBytes)).isEqualTo(AsmUtils.textify(outputBytes));
+  }
 }
diff --git a/javatests/com/google/turbine/lower/IntegrationTestSupport.java b/javatests/com/google/turbine/lower/IntegrationTestSupport.java
index 73a8f0e..0cc5e92 100644
--- a/javatests/com/google/turbine/lower/IntegrationTestSupport.java
+++ b/javatests/com/google/turbine/lower/IntegrationTestSupport.java
@@ -29,9 +29,9 @@
 import com.google.common.jimfs.Jimfs;
 import com.google.turbine.binder.Binder;
 import com.google.turbine.binder.ClassPathBinder;
-import com.google.turbine.bytecode.AsmUtils;
 import com.google.turbine.diag.SourceFile;
 import com.google.turbine.parse.Parser;
+import com.google.turbine.testing.AsmUtils;
 import com.google.turbine.tree.Tree;
 import com.sun.source.util.JavacTask;
 import com.sun.tools.javac.api.JavacTool;
diff --git a/javatests/com/google/turbine/lower/LowerTest.java b/javatests/com/google/turbine/lower/LowerTest.java
index a418a4e..16ea1ce 100644
--- a/javatests/com/google/turbine/lower/LowerTest.java
+++ b/javatests/com/google/turbine/lower/LowerTest.java
@@ -33,13 +33,13 @@
 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.bytecode.ByteReader;
 import com.google.turbine.bytecode.ConstantPoolReader;
 import com.google.turbine.model.TurbineConstantTypeKind;
 import com.google.turbine.model.TurbineFlag;
 import com.google.turbine.model.TurbineTyKind;
 import com.google.turbine.parse.Parser;
+import com.google.turbine.testing.AsmUtils;
 import com.google.turbine.type.Type;
 import java.io.IOException;
 import java.io.OutputStream;
diff --git a/javatests/com/google/turbine/bytecode/AsmUtils.java b/javatests/com/google/turbine/testing/AsmUtils.java
similarity index 97%
rename from javatests/com/google/turbine/bytecode/AsmUtils.java
rename to javatests/com/google/turbine/testing/AsmUtils.java
index 2591652..5b5e102 100644
--- a/javatests/com/google/turbine/bytecode/AsmUtils.java
+++ b/javatests/com/google/turbine/testing/AsmUtils.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.turbine.bytecode;
+package com.google.turbine.testing;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;