EnumCompiler.java

package com.mackenziehigh.autumn.lang.compiler.compilers;

import autumn.lang.compiler.ClassFile;
import autumn.lang.compiler.ast.nodes.EnumDefinition;
import autumn.lang.compiler.ast.nodes.Name;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.mackenziehigh.autumn.lang.compiler.typesystem.CustomDeclaredType;
import com.mackenziehigh.autumn.lang.compiler.typesystem.CustomField;
import com.mackenziehigh.autumn.lang.compiler.typesystem.CustomFormalParameter;
import com.mackenziehigh.autumn.lang.compiler.typesystem.CustomMethod;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IAnnotation;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IEnumConstant;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IField;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IFormalParameter;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IInterfaceType;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IMethod;
import com.mackenziehigh.autumn.lang.compiler.utils.Utils;
import com.mackenziehigh.autumn.resources.Finished;
import java.util.List;
import java.util.Set;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;

/**
 * An instance of this class controls the compilation of an enum-definition.
 *
 * @author Mackenzie High
 */
@Finished("2014/08/11")
final class EnumCompiler
        implements ICompiler
{
    /**
     * Essentially, this is the program that is being compiled.
     */
    public final ProgramCompiler program;

    /**
     * Essentially, this is the module that contains the enum-definition.
     */
    public final ModuleCompiler module;

    /**
     * This is the Abstract-Syntax-Tree representation of the enum-definition.
     */
    public final EnumDefinition node;

    /**
     * This will be the type-system representation of the enum-definition.
     *
     * This field is set during the type-declaration compiler pass.
     */
    public CustomDeclaredType type;

    /**
     * This is the type of the values() method.
     */
    private final CustomMethod method_values;

    /**
     * This is the type of the valueOf(String) method.
     */
    private final CustomMethod method_valueof;

    /**
     * Sole Constructor.
     *
     * @param module is the module that contains the enum being compiled.
     * @param node is the AST node that represents the enum being compiled.
     */
    public EnumCompiler(final ModuleCompiler module,
                        final EnumDefinition node)
    {
        Preconditions.checkNotNull(module);
        Preconditions.checkNotNull(node);

        this.program = module.program;
        this.module = module;
        this.node = node;

        this.method_values = new CustomMethod(program.typesystem.typefactory(), false);
        this.method_valueof = new CustomMethod(program.typesystem.typefactory(), false);
    }

    /**
     * This method generates the compiled class-file.
     *
     * @return the compiled class-file.
     */
    public ClassFile build()
    {
        final String enum_internal_name = Utils.internalName(type);

        final String enum_source_name = Utils.sourceName(type);

        /**
         * Create the bytecode representations of the enum-constants' fields.
         */
        final List<FieldNode> fields = Lists.newLinkedList();
        {
            for (IEnumConstant ec : type.getEnumConstants())
            {
                final int access = ec.getModifiers();
                final String name = ec.getName();
                final String desc = ec.getType().getDescriptor();
                final FieldNode field = new FieldNode(access, name, desc, null, null);

                fields.add(field);
            }
        }

        /**
         * Create the bytecode representation of the enum itself.
         */
        final ClassNode clazz = new ClassNode();
        {
            clazz.version = Opcodes.V1_6;
            clazz.visibleAnnotations = module.anno_utils.compileAnnotationList(type.getAnnotations());
            clazz.access = type.getModifiers();
            clazz.name = enum_internal_name;
            clazz.superName = Utils.internalName(type.getSuperclass());
            clazz.interfaces = Lists.newLinkedList();
            clazz.fields = fields;
            clazz.methods = Lists.newLinkedList();
            clazz.sourceFile = String.valueOf(node.getLocation().getFile());

            // Add some special methods to the enum.
            clazz.methods.add(generateStaticInitializer());
            clazz.methods.add(generateConstructor());
            clazz.methods.add(generateMethodValues());
            clazz.methods.add(generateMethodValueOf());
        }

        /**
         * Assemble the bytecode into an array of bytes.
         */
        final ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        clazz.accept(writer);
        final byte[] bytecode = writer.toByteArray();

        /**
         * Create the class-file object that will store the emitted bytecode.
         */
        final ClassFile file = new ClassFile(enum_source_name, bytecode);

        return file;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void performTypeDeclaration()
    {
        /**
         * Determine the descriptor of the enum.
         */
        final String namespace = module.type.getNamespace().replace('.', '/');
        final String name = node.getName().getName();
        final String descriptor = "L" + namespace + '/' + name + ';';

        /**
         * Ensure that the type was not already declared elsewhere.
         */
        program.checker.requireNonDuplicateType(node.getName(), descriptor);

        /**
         * Declare the enum.
         */
        this.type = program.typesystem.typefactory().newEnumType(descriptor);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void performTypeInitialization()
    {
        /**
         * Create the type-system representations of the annotation-list.
         */
        type.setAnnotations(module.anno_utils.typesOf(node.getAnnotations()));

        /**
         * Add a special annotation.
         */
        module.anno_utils.add(type, autumn.lang.internals.annotations.EnumDefinition.class);

        /**
         * Check the list of annotations.
         */
        program.checker.checkAnnotations(node.getAnnotations(), type.getAnnotations());

        /**
         * Set the type's access modifiers and make the type an enum.
         */
        type.setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER | Opcodes.ACC_ENUM);

        /**
         * Create the type-system representations of the enum-constant's field.
         */
        final List<IField> constants = Lists.newLinkedList();
        {
            int ordinal = 0;

            for (Name ec : node.getConstants())
            {
                final CustomField field = new CustomField(program.typesystem.typefactory());
                {
                    field.setOwner(type);
                    field.setModifiers(Opcodes.ACC_PUBLIC
                                       | Opcodes.ACC_STATIC
                                       | Opcodes.ACC_ENUM
                                       | Opcodes.ACC_FINAL);
                    field.setName(ec.getName());
                    field.setType(type);
                    field.setOrdinal(ordinal++);

                    constants.add(field);
                }
            }
        }

        /**
         * Initialize the type-system representation of the values() method.
         */
        method_values.setOwner(type);
        method_values.setAnnotations(ImmutableList.<IAnnotation>of());
        method_values.setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL);
        method_values.setName("values");
        method_values.setParameters(ImmutableList.<IFormalParameter>of());
        method_values.setReturnType(program.typesystem.typefactory().getArrayType(type, 1));

        /**
         * Initialize the type-system representation of the valueOf(String) method.
         */
        method_valueof.setOwner(type);
        method_valueof.setAnnotations(ImmutableList.<IAnnotation>of());
        method_valueof.setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL);
        method_valueof.setName("valueOf");
        final CustomFormalParameter param = new CustomFormalParameter();
        param.setType(program.typesystem.utils.STRING);
        method_valueof.setParameters(ImmutableList.<IFormalParameter>of(param));
        method_valueof.setReturnType(type);

        /**
         * Initialize the type that represents the enum being compiled.
         */
        type.setModifiers(Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_ENUM);
        type.setSuperclass(program.typesystem.utils.ENUM);
        type.setSuperinterfaces(ImmutableList.<IInterfaceType>of());
        type.setFields(constants);
        type.setMethods(ImmutableList.<IMethod>of(method_values, method_valueof));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void performTypeStructureChecking()
    {
        final Set<String> constants = Sets.newHashSet();

        for (Name name : node.getConstants())
        {
            /**
             * No two enum-constants can have the same name.
             */
            if (constants.contains(name.getName()))
            {
                program.checker.reportDuplicateEnumConstant(type, name);
            }
            else
            {
                constants.add(name.getName());
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void performTypeUsageChecking()
    {
        // Pass, because no type-usages exist to check.
    }

    /**
     * This method generates the static-initializer that initializes the enum-constant fields.
     *
     * @return the bytecode representation of the generated method.
     */
    private MethodNode generateStaticInitializer()
    {
        final MethodNode method = new MethodNode();
        method.access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;
        method.name = "<clinit>";
        method.desc = "()V";
        method.exceptions = ImmutableList.of();

        // Now bytecode will be generated that initializes the enum-constants' fields.

        for (IEnumConstant ec : type.getEnumConstants())
        {
            // Create an instance of the enum.
            method.instructions.add(new TypeInsnNode(Opcodes.NEW, Utils.internalName(type)));

            // Duplicate the enum object-reference on the operand-stack.
            method.instructions.add(new InsnNode(Opcodes.DUP));

            // Push the enum-constant's name onto the operand-stack.
            method.instructions.add(new LdcInsnNode(ec.getName()));

            // Push the enum-constant's ordinal onto the stack.
            method.instructions.add(new LdcInsnNode(ec.getOrdinal()));

            // Call the enum's instance constructor.
            method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL,
                                                       Utils.internalName(type),
                                                       "<init>",
                                                       "(Ljava/lang/String;I)V"));

            // Assign the enum object-reference to the enum-constant's field.
            method.instructions.add(new FieldInsnNode(Opcodes.PUTSTATIC,
                                                      Utils.internalName(type),
                                                      ec.getName(),
                                                      ec.getType().getDescriptor()));

            // Add NOPs to aid debugging bytecode.
            method.instructions.add(new InsnNode(Opcodes.NOP));
            method.instructions.add(new InsnNode(Opcodes.NOP));
        }

        // Return.
        method.instructions.add(new InsnNode(Opcodes.RETURN));

        return method;
    }

    /**
     * This method generates an instance constructor for the enum.
     *
     * @return the bytecode representation of the generated method.
     */
    private MethodNode generateConstructor()
    {
        final MethodNode method = new MethodNode();
        method.access = Opcodes.ACC_PRIVATE;
        method.name = "<init>";
        method.desc = "(Ljava/lang/String;I)V";
        method.exceptions = ImmutableList.of();

        // Invoke the super constructor.
        method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // load 'this'
        method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // load argument #1 (name)
        method.instructions.add(new VarInsnNode(Opcodes.ILOAD, 2)); // load argument #2 (ordinal)
        method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL,
                                                   "java/lang/Enum",
                                                   "<init>",
                                                   "(Ljava/lang/String;I)V"));

        // Return.
        method.instructions.add(new InsnNode(Opcodes.RETURN));

        return method;
    }

    /**
     * This method generates the values() method for the enum.
     *
     * @return the bytecode representation of the generated method.
     */
    private MethodNode generateMethodValues()
    {
        final MethodNode method = Utils.bytecodeOf(module, method_values);

        // Now bytecode will be generated that creates an array and then returns it.

        // Push the size of the array onto the operand-stack.
        method.instructions.add(new LdcInsnNode(type.getEnumConstants().size()));

        // Create the array that will contain the enum-constants.
        method.instructions.add(new TypeInsnNode(Opcodes.ANEWARRAY, Utils.internalName(type)));

        // Push the initial value of a counter onto the operand-stack.
        method.instructions.add(new LdcInsnNode(0));

        // Add NOPs to aid debugging bytecode.
        method.instructions.add(new InsnNode(Opcodes.NOP));
        method.instructions.add(new InsnNode(Opcodes.NOP));

        for (IEnumConstant ec : type.getEnumConstants())
        {
            // Duplicate the top two elements on the operand-stack.
            // Specifically, duplicate the array-reference and the counter.
            method.instructions.add(new InsnNode(Opcodes.DUP2));

            // Get the value stored in the enum-constant's field.
            method.instructions.add(new FieldInsnNode(Opcodes.GETSTATIC,
                                                      Utils.internalName(type),
                                                      ec.getName(),
                                                      ec.getType().getDescriptor()));

            // Put the value from the enum-constant's field into the array.
            // The index of the array element is the counter.
            method.instructions.add(new InsnNode(Opcodes.AASTORE));

            // Increment the counter, by one.
            method.instructions.add(new LdcInsnNode(1));
            method.instructions.add(new InsnNode(Opcodes.IADD));

            // Add NOPs to aid debugging bytecode.
            method.instructions.add(new InsnNode(Opcodes.NOP));
            method.instructions.add(new InsnNode(Opcodes.NOP));
        }

        // Pop the counter off of the operand-stack.
        method.instructions.add(new InsnNode(Opcodes.POP));

        // Return the array.
        method.instructions.add(new InsnNode(Opcodes.ARETURN));

        return method;
    }

    /**
     * This method generates the valueOf(String) method for the enum.
     *
     * @return the bytecode representation of the generated method.
     */
    private MethodNode generateMethodValueOf()
    {
        final MethodNode method = Utils.bytecodeOf(module, method_valueof);

        // Now bytecode will be generated that searches for the specified enum-constant.

        // Get an array containing all of the num-constants, by calling the values() method.
        method.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
                                                   Utils.internalName(type),
                                                   method_values.getName(),
                                                   method_values.getDescriptor()));

        // Load the name of the enum-constant to search for onto the operand-stack.
        method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));

        // Call a helper method that will perform the search.
        method.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
                                                   Utils.internalName(program.typesystem.utils.HELPERS),
                                                   "findEnumConstant",
                                                   "([Ljava/lang/Enum;Ljava/lang/String;)Ljava/lang/Enum;"));

        // Cast the return-value of the helper method to the type of the enum being compiled.
        // Note: This cast will always succeed at runtime.
        method.instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, Utils.internalName(type)));

        // Return the enum-constant that was found.
        method.instructions.add(new InsnNode(Opcodes.ARETURN));

        return method;
    }
}