AbstractTypeChecker.java

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

import autumn.lang.annotations.Infer;
import autumn.lang.compiler.ast.commons.IBinaryOperation;
import autumn.lang.compiler.ast.commons.IExpression;
import autumn.lang.compiler.ast.commons.IUnaryOperation;
import autumn.lang.compiler.ast.nodes.Name;
import autumn.lang.compiler.ast.nodes.TypeSpecifier;
import autumn.lang.compiler.ast.nodes.Variable;
import autumn.lang.internals.Operators;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IClassType;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IDeclaredType;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IExpressionType;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IField;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IMethod;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IReturnType;
import com.mackenziehigh.autumn.lang.compiler.typesystem.design.IType;
import com.mackenziehigh.autumn.lang.compiler.utils.TypeSystemUtils;
import java.util.Collections;
import java.util.List;

/**
 * This class provides methods needed by all type-checkers.
 *
 * @author Mackenzie High
 */
abstract class AbstractTypeChecker
        extends AbstractAstVisitor
{
    /**
     * Essentially, this method retrieves the enclosing function.
     *
     * @return the enclosing function compiler.
     */
    public abstract AbstractFunctionCompiler function();

    /**
     * This is the compiler that is responsible for compiling the enclosing program.
     */
    protected final ProgramCompiler program;

    /**
     * This is the compiler that is responsible for compiling the enclosing module.
     */
    protected final ModuleCompiler module;

    /**
     * Sole Constructor.
     *
     * @param function is the compiler that is responsible for compiling the enclosing function.
     */
    protected AbstractTypeChecker(final AbstractFunctionCompiler function)
    {
        Preconditions.checkNotNull(function);

        this.program = function.module.program;
        this.module = function.module;
    }

    /**
     * This method binds a type to an abstract-syntax-tree node.
     *
     * @param node is the AST node that is being typed.
     * @param type is the type of the construct represented by the AST node.
     */
    protected void infer(final IExpression node,
                         final IExpressionType type)
    {
        Preconditions.checkNotNull(node);
        Preconditions.checkNotNull(type);

        program.symbols.expressions.put(node, type);
    }

    /**
     * This method type-checks a condition expression.
     *
     * <p>
     * If the type-check fails, an error-report will be issued by the compiler.
     * </p>
     *
     * @param expression is the expression that is a condition.
     */
    protected void condition(final IExpression expression)
    {
        Preconditions.checkNotNull(expression);

        expression.accept(this);

        program.checker.checkCondition(expression);
    }

    /**
     * This method requires that the type of an expression is a subtype of a given type.
     *
     * <p>
     * If the type-check fails, an error-report will be issued by the compiler.
     * </p>
     *
     * @param expected is the expected type of the expression.
     * @param expression is the expression to type-check.
     */
    protected void assertType(final IReturnType expected,
                              final IExpression expression)
    {
        expression.accept(this);

        program.checker.expectSubtype(expression, expected);
    }

    /**
     * This method generalizes the resolution and type-checking of a unary operator.
     *
     * <p>
     * If the type-checking fails, an error-report will be issued by the compiler.
     * </p>
     *
     * @param operation is the unary operation itself.
     * @param function is the name of the static utility method that implements the operation.
     * @param operand is the only operand of the operation.
     */
    protected void unaryOperation(final IUnaryOperation operation,
                                  final String function,
                                  final IExpression operand)
    {
        Preconditions.checkNotNull(operation);
        Preconditions.checkNotNull(function);
        Preconditions.checkNotNull(operand);

        // Perform type-checking on the left-operand.
        operand.accept(this);
        final IExpressionType operand_type = program.symbols.expressions.get(operand);

        // This is the type of the Operators class, which provides the operation implementations.
        final IDeclaredType owner = (IDeclaredType) program.typesystem
                .typefactory()
                .fromClass(Operators.class);

        // Resolve the applicable overloads of the operator method.
        final List<IMethod> overloads = program.typesystem.utils.resolveStaticMethods(module.type,
                                                                                      owner,
                                                                                      function,
                                                                                      Collections.<IType>singletonList(operand_type));

        // If no applicable overload was found,
        // then issue an error-report,
        // because a bad operand was provided by the user.
        if (overloads.isEmpty())
        {
            // This metohd will throw an exception.
            program.checker.reportNoSuchUnaryOperator(operation, operand_type);
        }

        // The first method is the most specific.
        final IMethod overload = overloads.get(0);

        // Remember the overload, because the code generator will need it later.
        program.symbols.calls.put(operation, overload);

        // The return-type of the selected overload is the return-type of the expression.
        infer(operation, overload.getReturnType());
    }

    /**
     * This method generalizes the resolution and type-checking of a binary operator.
     *
     * <p>
     * If the type-checking fails, an error-report will be issued by the compiler.
     * </p>
     *
     * @param operation is the binary operation itself.
     * @param function is the name of the static utility method that implements the operation.
     * @param operand is the only operand of the operation.
     */
    protected void binaryOperation(final IBinaryOperation operation,
                                   final String function,
                                   final IExpression left,
                                   final IExpression right)
    {
        Preconditions.checkNotNull(operation);
        Preconditions.checkNotNull(function);
        Preconditions.checkNotNull(left);
        Preconditions.checkNotNull(right);

        // These are the types of the operands.
        final List<IType> operands = Lists.newLinkedList();

        // Perform type-checking on the left-operand.
        left.accept(this);
        final IExpressionType left_type = program.symbols.expressions.get(left);
        operands.add(left_type);

        // Perform type-checking on the right-operand.
        right.accept(this);
        final IExpressionType right_type = program.symbols.expressions.get(right);
        operands.add(right_type);

        // This is the type of the Operators class, which provides the operation implementations.
        final IDeclaredType owner = (IDeclaredType) program.typesystem
                .typefactory()
                .fromClass(Operators.class);

        // Resolve the applicable overloads of the operator method.
        final List<IMethod> overloads = program.typesystem.utils.resolveStaticMethods(module.type,
                                                                                      owner,
                                                                                      function,
                                                                                      operands);

        // If no applicable overload was found,
        // then issue an error-report,
        // because bad operands were provided by the user.
        if (overloads.isEmpty())
        {
            // This metohd will throw an exception.
            program.checker.reportNoSuchBinaryOperator(operation, left_type, right_type);
        }

        // The first method is the most specific.
        final IMethod overload = overloads.get(0);

        // Remember the overload, because the code generator will need it later.
        program.symbols.calls.put(operation, overload);

        // The return-type of the selected overload is the return-type of the expression.
        infer(operation, overload.getReturnType());
    }

    /**
     * This method generalizes the resolution and type-checking of a static method invocation.
     *
     * @param operation is the invocation expression himself.
     * @param owner specifies the type that declares the invoked method.
     * @param name is the name of the invoked method.
     * @param arguments are the expressions that produce the invoked method's arguments.
     */
    protected void callStaticMethod(final IExpression operation,
                                    final TypeSpecifier owner,
                                    final Name name,
                                    final Iterable<IExpression> arguments)
    {
        /**
         * Type-check the owner.
         */
        final IDeclaredType owner_type = module.imports.resolveDeclaredType(owner);

        /**
         * Visit and type-check the arguments.
         */
        final List<IExpressionType> args = Lists.newLinkedList();

        for (IExpression arg : arguments)
        {
            arg.accept(this);
            args.add(program.symbols.expressions.get(arg));
        }

        /**
         * Resolve the method overload.
         */
        final List<IMethod> methods = program.typesystem.utils.resolveStaticMethods(module.type,
                                                                                    owner_type,
                                                                                    name.getName(),
                                                                                    args);

        /**
         * If no method overload could be found, issue an error.
         */
        if (methods.isEmpty())
        {
            program.checker.reportNoSuchMethod(operation, true, owner_type, name.getName(), args);
        }

        /**
         * Remember the resolved method overload, because the code-generator will need it.
         */
        final IMethod method = (IMethod) methods.get(0);
        program.symbols.calls.put(operation, method);

        /**
         * The return-type of a method-invocation is usually the return-type of the invoked method.
         * However, sometimes the return-type of the method-invocation is inferred.
         *
         * Note: We cannot infer the return-type when there are no arguments, even if requested.
         */
        if (method.getParameters().isEmpty())
        {
            infer(operation, method.getReturnType());
        }
        else if (TypeSystemUtils.isAnnotationPresent(method, Infer.class))
        {
            infer(operation, args.get(0));
        }
        else
        {
            infer(operation, method.getReturnType());
        }
    }

    /**
     * This method performs the resolution of a static-field.
     *
     * @param site is the site where the field is being used.
     * @param type is the type that contains the field.
     * @param name is the name of the field.
     * @return the field that was found, or null, if no field was found.
     */
    protected IField findStaticField(final IExpression site,
                                     final TypeSpecifier type,
                                     final String name)
    {
        /**
         * Get the type of the enclosing module in order to perform access checking.
         */
        final IClassType user = module.type;

        /**
         * Get the type that owns the field.
         *
         * This will throw an exception, if the type does not exist
         * or type is not a declared-type.
         */
        final IDeclaredType owner = module.imports.resolveDeclaredType(type);

        /**
         * Resolve the field.
         */
        final IField field = program.typesystem.utils.resolveStaticField(user,
                                                                         owner,
                                                                         name);

        /**
         * If no field was found, then issue an error.
         */
        if (field == null)
        {
            // This will throw an exception.
            program.checker.reportNoSuchField(site, true, owner, name);
        }

        /**
         * Remember the field that is being set, so that the code-generator can emit bytecode.
         */
        program.symbols.fields.put(site, field);

        return field;
    }

    /**
     * This method performs the resolution of an instance-field.
     *
     * @param site is the site where the field is being used.
     * @param object is the expression that produces the owner of the field.
     * @param name is the name of the field.
     * @return the field that was found, or null, if no field was found.
     */
    protected IField findField(final IExpression site,
                               final IExpression object,
                               final String name)
    {
        /**
         * Get the type of the enclosing module in order to perform access checking.
         */
        final IClassType user = module.type;

        /**
         * Get the type that owns the field.
         */
        final IExpressionType owner_expr_type = program.symbols.expressions.get(object);

        // This will throw an exception, if the owner-type is not a declared-type.
        program.checker.requireDeclaredType(object, owner_expr_type);

        // This never fails.
        final IDeclaredType owner = (IDeclaredType) owner_expr_type;

        /**
         * Resolve the field.
         */
        final IField field = program.typesystem.utils.resolveField(user,
                                                                   owner,
                                                                   name);

        /**
         * If no field was found, then issue an error.
         */
        if (field == null)
        {
            // This will throw an exception.
            program.checker.reportNoSuchField(site, false, owner, name);
        }

        /**
         * Remember the field that is being set, so that the code-generator can emit bytecode.
         */
        program.symbols.fields.put(site, field);

        return field;
    }

    /**
     * This method declares a local-variable and type-checks the declaration.
     *
     * @param variable is the variable to declare.
     * @param type is the type of the new variable.
     * @param mutable is false, iff the variable is readonly.
     */
    protected void declareVar(final Variable variable,
                              final IExpressionType type,
                              final boolean mutable)
    {
        /**
         * The variable cannot be declared, if another variable was already created
         * with the same name.
         */
        final boolean alread_declared = function().allocator.isDeclared(variable.getName());
        program.checker.reportDuplicateVariable(variable, alread_declared);

        /**
         * The type of a variable must be a variable-type.
         */
        program.checker.requireVariableType(variable, type);

        /**
         * Perform the actual variable declaration.
         */
        if (mutable)
        {
            function().allocator.declareVar(variable.getName(), type);
        }
        else
        {
            function().allocator.declareVal(variable.getName(), type);
        }
    }
}