# adapted from
# http://smallshire.org.uk/sufficientlysmall/2010/04/11/\
#       a-hindley-milner-type-inference-implementation-in-python/

import gast as ast
from copy import deepcopy

from numpy import floating, integer, complexfloating

from pythran.tables import MODULES, attributes
import pythran.typing as typing
from pythran.syntax import PythranSyntaxError
from pythran.utils import isnum


class PythranTypeError(PythranSyntaxError):
    "A new type to distinguish general syntax errors from typing issues"


class InferenceError(Exception):
    "Raised if the type inference algorithm cannot infer types successfully"


symbol_of = {
    ast.And: 'and',
    ast.Or: 'or',
    ast.Add: '+',
    ast.Sub: '-',
    ast.Mult: '*',
    ast.Div: '/',
    ast.Mod: '%',
    ast.Pow: '**',
    ast.LShift: '<<',
    ast.RShift: '>>',
    ast.BitOr: '|',
    ast.BitXor: '^',
    ast.BitAnd: '&',
    ast.FloorDiv: '//',
    ast.Invert: '~',
    ast.MatMult: '@',
    ast.Not: '!',
    ast.UAdd: '+',
    ast.USub: '-',
}

NoneType_ = type(None)

# =======================================================#
# Types and type constructors


class TypeVariable(object):
    """A type variable standing for an arbitrary type.

    All type variables have a unique id, but names are only assigned lazily,
    when required.
    """

    _cached_names = {}

    def __init__(self):
        self.instance = None
        self.name = None

    def __str__(self):
        if self.instance:
            return str(self.instance)
        else:
            return 'T{}'.format(
                TypeVariable._cached_names.setdefault(
                    self,
                    len(TypeVariable._cached_names)
                )
            )


class TypeOperator(object):
    """An n-ary type constructor which builds a new type from old"""

    def __init__(self, name, types):
        self.name = name
        self.types = types

    def __str__(self):
        num_types = len(self.types)
        if num_types == 0:
            return self.name
        elif self.name == 'fun':
            return 'Callable[[{0}], {1}]'.format(
                ', '.join(map(str, self.types[:-1])), self.types[-1])
        elif self.name == 'option':
            return 'Option[{0}]'.format(self.types[0])
        else:
            return "{0}[{1}]" .format(self.name.capitalize(),
                                      ', '.join(map(str, self.types)))


class Collection(TypeOperator):

    def __init__(self, holder_type, key_type, value_type, iter_type):
        super(Collection, self).__init__("collection",
                                         [holder_type, key_type, value_type,
                                          iter_type])

    def __str__(self):
        t0 = prune(self.types[0])
        if isinstance(t0, TypeVariable):
            if isinstance(prune(self.types[1]), TypeVariable):
                return 'Iterable[{}]'.format(self.types[3])
            else:
                return 'Collection[{}, {}]'.format(self.types[1],
                                                   self.types[2])
        if isinstance(t0, TypeOperator) and t0.name == 'traits':
            if all(isinstance(prune(t), TypeVariable) for t in t0.types):
                return 'Collection[{}, {}]'.format(self.types[1],
                                                   self.types[2])
            elif all(isinstance(prune(t), TypeVariable)
                     for t in t0.types[:1] + t0.types[2:]):
                t01 = prune(t0.types[1])
                if isinstance(t01, TypeOperator) and t01.name == LenTrait.name:
                    return 'Sized'
        t00 = prune(t0.types[0])
        if isinstance(t00, TypeOperator):
            type_trait = t00.name
            if type_trait == 'list':
                return 'List[{}]'.format(self.types[2])
            if type_trait == 'set':
                return 'Set[{}]'.format(self.types[2])
            if type_trait == 'dict':
                return 'Dict[{}, {}]'.format(self.types[1], self.types[2])
            if type_trait == 'str':
                return 'str'
            if type_trait == 'file':
                return 'IO[str]'
            if type_trait == 'tuple':
                return 'Tuple[{}]'.format(', '.join(map(str, self.types[1:])))
            if type_trait == 'array':
                t01 = prune(t0.types[1])
                hasnolen = (isinstance(t01, TypeOperator) and
                            t01.name == NoLenTrait.name)
                if hasnolen:
                    return str(self.types[2])

                def rec(n):
                    pn = prune(n)
                    if isinstance(pn, Collection):
                        traits = prune(pn.types[0])
                        # a scalar or array?
                        if isinstance(traits, TypeVariable):
                            return pn.types[3], 0
                        len_trait = prune(traits.types[1])
                        # an array?
                        haslen = (isinstance(len_trait, TypeOperator) and
                                  len_trait.name == LenTrait.name)
                        if haslen:
                            t, n = rec(pn.types[3])
                            return t, n + 1
                        # a scalar or array?
                        else:
                            return pn.types[2], 0
                    else:
                        return pn, 0
                t, n = rec(self)
                if isinstance(t, TypeVariable):
                    return 'Array[{} d+, {}]'.format(n, t)
                else:
                    return 'Array[{}d, {}]'.format(n, t)
            if type_trait == 'gen':
                return 'Generator[{}]'.format(self.types[2])
        return super(Collection, self).__str__()


def TupleTrait(of_types):
    return TypeOperator('tuple', of_types)


ListTrait = TypeOperator('list', [])
SetTrait = TypeOperator('set', [])
DictTrait = TypeOperator('dict', [])
StrTrait = TypeOperator('str', [])
FileTrait = TypeOperator('file', [])
ArrayTrait = TypeOperator('array', [])
GenerableTrait = TypeOperator('gen', [])

LenTrait = TypeOperator("len", [])
NoLenTrait = TypeOperator("no_len", [])
SliceTrait = TypeOperator("slice", [])
NoSliceTrait = TypeOperator("no_slice", [])


def File():
    return Collection(Traits([FileTrait, NoLenTrait, NoSliceTrait]),
                      InvalidKey, Str(), Str())


def List(of_type):
    return Collection(Traits([ListTrait, LenTrait, SliceTrait]),
                      Integer(), of_type, of_type)


def Set(of_type):
    return Collection(Traits([SetTrait, LenTrait, NoSliceTrait]),
                      InvalidKey, of_type, of_type)


def Dict(key_type, value_type):
    return Collection(Traits([DictTrait, LenTrait, NoSliceTrait]),
                      key_type, value_type, key_type)


def Str(rec=6):
    Next = Str(rec - 1) if rec else TypeVariable()
    return Collection(Traits([StrTrait, LenTrait, SliceTrait]),
                      Integer(),
                      Next,
                      Next)


def Array(of_type, dim):
    return Collection(Traits([ArrayTrait, LenTrait, SliceTrait]),
                      AnyType,
                      AnyType,
                      Array(of_type, dim - 1) if dim > 1 else of_type)


def Iterable(of_type, dim):
    return Collection(Traits([TypeVariable(), LenTrait, SliceTrait]),
                      AnyType,
                      AnyType,
                      Iterable(of_type, dim - 1) if dim > 1 else of_type)


def Generator(of_type):
    return Collection(Traits([GenerableTrait, NoLenTrait, NoSliceTrait]),
                      InvalidKey, of_type, of_type)


def Tuple(of_types):
    return Collection(Traits([TupleTrait(of_types), LenTrait, SliceTrait]),
                      Integer(), TypeVariable(), TypeVariable())


class Scalar(TypeOperator):
    def __init__(self, types=None):
        if not isinstance(types, list):
            dtype = types
            if dtype == 'complex':
                types = [ComplexTrait,    TypeVariable(),
                         TypeVariable(), TypeVariable()]
            elif dtype == 'float':
                types = [TypeVariable(),    FloatTrait,
                         TypeVariable(), TypeVariable()]
            elif dtype == 'int':
                types = [TypeVariable(),  TypeVariable(),
                         IntegerTrait, TypeVariable()]
            elif dtype == 'bool':
                types = [TypeVariable(),  TypeVariable(),
                         TypeVariable(),      BoolTrait]
            else:
                assert dtype is None
                types = [TypeVariable(), TypeVariable(),
                         TypeVariable(), TypeVariable()]
        super(Scalar, self).__init__('scalar', types)

    def __str__(self):
        if isinstance(prune(self.types[0]), TypeOperator):
            return 'complex'
        if isinstance(prune(self.types[1]), TypeOperator):
            return 'float'
        if isinstance(prune(self.types[2]), TypeOperator):
            return 'int'
        if isinstance(prune(self.types[3]), TypeOperator):
            return 'bool'
        return 'Scalar'


def Complex():
    return Collection(Traits([ArrayTrait, NoLenTrait, NoSliceTrait]),
                      InvalidKey, Scalar('complex'), InvalidKey)


def Float():
    return Collection(Traits([ArrayTrait, NoLenTrait, NoSliceTrait]),
                      InvalidKey, Scalar('float'), InvalidKey)


def Integer():
    return Collection(Traits([ArrayTrait, NoLenTrait, NoSliceTrait]),
                      InvalidKey, Scalar('int'), InvalidKey)


def Bool():
    return Collection(Traits([ArrayTrait, NoLenTrait, NoSliceTrait]),
                      InvalidKey, Scalar('bool'), InvalidKey)


def DType():
    return Collection(Traits([ArrayTrait, NoLenTrait, NoSliceTrait]),
                      InvalidKey, Scalar(), InvalidKey)


def Function(from_types, to_type):
    """A binary type constructor which builds function types"""
    return TypeOperator('fun', list(from_types) + [to_type])


def OptionType(of_type):
    return TypeOperator("option", [of_type])


def Traits(of_types):
    return TypeOperator("traits", of_types)


ExceptionType = TypeOperator("exception", [])

# Basic types are constructed with a null type constructor
IntegerTrait = TypeOperator("int", [])  # any integer
FloatTrait = TypeOperator("float", [])  # any float
ComplexTrait = TypeOperator("complex", [])
BoolTrait = TypeOperator("bool", [])
InvalidKey = TypeOperator("invalid-key", [])  # for non-indexable collection
NoneType = TypeOperator("none", [])
AnyType = TypeOperator("any", [])
InvalidType = TypeOperator("invalid-type", [])
Slice = TypeOperator("slice", [])  # slice


def is_none(t):
    pt = prune(t)
    return isinstance(pt, TypeOperator) and pt.name == "none"


def is_option_type(t):
    pt = prune(t)
    return isinstance(pt, TypeOperator) and pt.name == "option"


def maybe_array_type(t):
    pt = prune(t)
    if isinstance(pt, TypeVariable):
        return True  # maybe an array :-/
    if isinstance(pt, TypeOperator) and pt.name == "collection":
        st = prune(pt.types[0])
        if isinstance(st, TypeOperator) and st.name == "traits":
            tt = prune(st.types[0])
            if isinstance(tt, TypeVariable):
                return True  # maybe
            return isinstance(tt, TypeOperator) and tt.name == "array"
    return False


def is_test_is_none(node):
    if not isinstance(node, ast.Compare):
        return False
    left = node.left
    comparators = node.comparators
    ops = node.ops
    if len(ops) != 1:
        return False

    op = ops[0]
    if type(op) not in (ast.Is, ast.Eq):
        return False

    comparator = comparators[0]
    if not isinstance(comparator, ast.Attribute):
        return False

    return comparator.attr == 'None' and isinstance(left, ast.Name)


def is_tuple_type(t):
    pt = prune(t)
    if isinstance(pt, TypeOperator) and pt.name == "collection":
        st = prune(pt.types[0])
        if isinstance(st, TypeOperator) and st.name == "traits":
            tt = prune(st.types[0])
            return isinstance(tt, TypeOperator) and tt.name == "tuple"
    return False


def is_getattr(node):
    if not isinstance(node, ast.Call):
        return False
    if not isinstance(node.func, ast.Attribute):
        return False
    return node.func.attr == 'getattr'


class MultiType(object):
    """A binary type constructor which builds function types"""

    def __init__(self, types):
        self.name = 'multitype'
        self.types = types

    def __str__(self):
        return '\n'.join(sorted(map(str, self.types)))


def tr(t):
    def rec_tr(t, env):
        if isinstance(t, typing.TypeVar):
            if t in env:
                return env[t]
            else:
                env[t] = TypeVariable()
                return env[t]

        elif t is typing.Any:
            return TypeVariable()

        elif isinstance(t, NoneType_):
            return NoneType

        elif t is bool:
            return Bool()

        elif issubclass(t, slice):
            return Slice

        elif issubclass(t, (complex, complexfloating)):
            return Complex()

        elif issubclass(t, (float, floating)):
            return Float()

        elif issubclass(t, (int, integer)):
            return Integer()

        elif issubclass(t, NoneType_):
            return NoneType

        elif t is str:
            return Str()

        elif isinstance(t, typing.Generator):
            return Generator(rec_tr(t.__args__[0], env))

        elif isinstance(t, typing.List):
            return List(rec_tr(t.__args__[0], env))

        elif isinstance(t, typing.Optional):
            return OptionType(rec_tr(t.__args__[0], env))

        elif isinstance(t, typing.Set):
            return Set(rec_tr(t.__args__[0], env))

        elif isinstance(t, typing.Dict):
            return Dict(rec_tr(t.__args__[0], env), rec_tr(t.__args__[1], env))

        elif isinstance(t, typing.Tuple):
            return Tuple([rec_tr(tp, env) for tp in t.__args__])

        elif isinstance(t, typing.NDArray):
            return Array(rec_tr(t.__args__[0], env), len(t.__args__[1:]))

        elif isinstance(t, typing.Pointer):
            return Array(rec_tr(t.__args__[0], env), 1)

        elif isinstance(t, typing.Union):
            return MultiType([rec_tr(ut, env) for ut in t.__args__])

        elif t is typing.File:
            return File()

        elif isinstance(t, typing.Iterable):
            return Collection(TypeVariable(), TypeVariable(), TypeVariable(),
                              rec_tr(t.__args__[0], env))

        elif t is typing.Sized:
            return Collection(
                Traits([TypeVariable(), LenTrait, TypeVariable()]),
                TypeVariable(), TypeVariable(), TypeVariable()
            )

        elif isinstance(t, typing.Fun):
            return Function([rec_tr(at, env) for at in t.__args__[:-1]],
                            rec_tr(t.__args__[-1], env))

        else:
            raise NotImplementedError(t)

    if isinstance(t, dict):
        return t
    elif hasattr(t, 'signature'):
        return rec_tr(t.signature, {})
    else:
        return rec_tr(t, {})


####


def analyse_body(body, env, non_generic):
    # first step to gather global symbols
    for stmt in body:
        if isinstance(stmt, ast.FunctionDef):
            new_type = TypeVariable()
            env[stmt.name] = new_type
    # second to perform local inference
    for stmt in body:
        analyse(stmt, env, non_generic)


class HasYield(ast.NodeVisitor):

    def __init__(self):
        super(HasYield, self).__init__()
        self.has_yield = False

    def visit_FunctionDef(self, node):
        pass

    def visit_Yield(self, node):
        self.has_yield = True


def analyse(node, env, non_generic=None):
    """Computes the type of the expression given by node.

    The type of the node is computed in the context of the context of the
    supplied type environment env. Data types can be introduced into the
    language simply by having a predefined set of identifiers in the initial
    environment. Environment; this way there is no need to change the syntax
    or more importantly, the type-checking program when extending the language.

    Args:
        node: The root of the abstract syntax tree.
        env: The type environment is a mapping of expression identifier names
            to type assignments.
        non_generic: A set of non-generic variables, or None

    Returns:
        The computed type of the expression.

    Raises:
        InferenceError: The type of the expression could not be inferred,
        PythranTypeError: InferenceError with user friendly message + location
    """

    if non_generic is None:
        non_generic = set()

    # expr
    if isinstance(node, ast.Name):
        if isinstance(node.ctx, (ast.Store)):
            new_type = TypeVariable()
            non_generic.add(new_type)
            env[node.id] = new_type
        return get_type(node.id, env, non_generic)
    elif isinstance(node, ast.Constant):
        if isinstance(node.value, str):
            return Str()
        elif isinstance(node.value, int):
            return Integer()
        elif isinstance(node.value, float):
            return Float()
        elif isinstance(node.value, complex):
            return Complex()
        elif node.value is None:
            return NoneType
        else:
            raise NotImplementedError
    elif isinstance(node, ast.Compare):
        left_type = analyse(node.left, env, non_generic)
        comparators_type = [analyse(comparator, env, non_generic)
                            for comparator in node.comparators]
        ops_type = [analyse(op, env, non_generic)
                    for op in node.ops]
        prev_type = left_type
        result_type = TypeVariable()
        for op_type, comparator_type in zip(ops_type, comparators_type):
            try:
                unify(Function([prev_type, comparator_type], result_type),
                      op_type)
                prev_type = comparator_type
            except InferenceError:
                raise PythranTypeError(
                    "Invalid comparison, between `{}` and `{}`".format(
                        prev_type,
                        comparator_type
                    ),
                    node)
        return result_type
    elif isinstance(node, ast.Call):
        if is_getattr(node):
            self_type = analyse(node.args[0], env, non_generic)
            attr_name = node.args[1].value
            _, attr_signature = attributes[attr_name]
            attr_type = tr(attr_signature)
            result_type = TypeVariable()
            try:
                unify(Function([self_type], result_type), attr_type)
            except InferenceError:
                if isinstance(prune(attr_type), MultiType):
                    msg = 'no attribute found, tried:\n{}'.format(attr_type)
                else:
                    msg = 'tried {}'.format(attr_type)
                raise PythranTypeError(
                    "Invalid attribute for getattr call with self"
                    "of type `{}`, {}".format(self_type, msg), node)

        else:
            fun_type = analyse(node.func, env, non_generic)
            arg_types = [analyse(arg, env, non_generic) for arg in node.args]
            result_type = TypeVariable()
            try:
                unify(Function(arg_types, result_type), fun_type)
            except InferenceError:
                # recover original type
                fun_type = analyse(node.func, env, non_generic)
                if isinstance(prune(fun_type), MultiType):
                    msg = 'no overload found, tried:\n{}'.format(fun_type)
                else:
                    msg = 'tried {}'.format(fun_type)
                raise PythranTypeError(
                    "Invalid argument type for function call to "
                    "`Callable[[{}], ...]`, {}"
                    .format(', '.join('{}'.format(at) for at in arg_types),
                            msg),
                    node)
        return result_type

    elif isinstance(node, ast.IfExp):
        test_type = analyse(node.test, env, non_generic)
        unify(Function([test_type], Bool()),
              tr(MODULES['builtins']['bool']))

        if is_test_is_none(node.test):
            none_id = node.test.left.id
            body_env = env.copy()
            body_env[none_id] = NoneType
        else:
            none_id = None
            body_env = env

        body_type = analyse(node.body, body_env, non_generic)

        if none_id:
            orelse_env = env.copy()
            if is_option_type(env[none_id]):
                orelse_env[none_id] = prune(env[none_id]).types[0]
            else:
                orelse_env[none_id] = TypeVariable()
        else:
            orelse_env = env

        orelse_type = analyse(node.orelse, orelse_env, non_generic)

        try:
            return merge_unify(body_type, orelse_type)
        except InferenceError:
            raise PythranTypeError(
                "Incompatible types from different branches:"
                "`{}` and `{}`".format(
                    body_type,
                    orelse_type
                ),
                node
            )
    elif isinstance(node, ast.UnaryOp):
        operand_type = analyse(node.operand, env, non_generic)
        op_type = analyse(node.op, env, non_generic)
        result_type = TypeVariable()
        try:
            unify(Function([operand_type], result_type), op_type)
            return result_type
        except InferenceError:
            raise PythranTypeError(
                "Invalid operand for `{}`: `{}`".format(
                    symbol_of[type(node.op)],
                    operand_type
                ),
                node
            )
    elif isinstance(node, ast.BinOp):
        left_type = analyse(node.left, env, non_generic)
        op_type = analyse(node.op, env, non_generic)
        right_type = analyse(node.right, env, non_generic)
        result_type = TypeVariable()
        try:
            unify(Function([left_type, right_type], result_type), op_type)
        except InferenceError:
            raise PythranTypeError(
                "Invalid operand for `{}`: `{}` and `{}`".format(
                    symbol_of[type(node.op)],
                    left_type,
                    right_type),
                node
            )
        return result_type
    elif isinstance(node, ast.Pow):
        return tr(MODULES['numpy']['power'])
    elif isinstance(node, ast.Sub):
        return tr(MODULES['operator']['sub'])
    elif isinstance(node, (ast.USub, ast.UAdd)):
        return tr(MODULES['operator']['pos'])
    elif isinstance(node, (ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt,
                           ast.GtE, ast.Is, ast.IsNot)):
        return tr(MODULES['operator']['eq'])
    elif isinstance(node, (ast.In, ast.NotIn)):
        contains_sig = tr(MODULES['operator']['contains'])
        contains_sig.types[:-1] = reversed(contains_sig.types[:-1])
        return contains_sig
    elif isinstance(node, ast.Add):
        return tr(MODULES['operator']['add'])
    elif isinstance(node, ast.Mult):
        return tr(MODULES['operator']['mul'])
    elif isinstance(node, ast.MatMult):
        return tr(MODULES['operator']['matmul'])
    elif isinstance(node, (ast.Div, ast.FloorDiv)):
        return tr(MODULES['operator']['floordiv'])
    elif isinstance(node, ast.Mod):
        return tr(MODULES['operator']['mod'])
    elif isinstance(node, (ast.LShift, ast.RShift)):
        return tr(MODULES['operator']['lshift'])
    elif isinstance(node, (ast.BitXor, ast.BitAnd, ast.BitOr)):
        return tr(MODULES['operator']['lshift'])
    elif isinstance(node, ast.List):
        new_type = TypeVariable()
        for elt in node.elts:
            elt_type = analyse(elt, env, non_generic)
            try:
                unify(new_type, elt_type)
            except InferenceError:
                raise PythranTypeError(
                    "Incompatible list element type `{}` and `{}`".format(
                        new_type, elt_type),
                    node
                )
        return List(new_type)
    elif isinstance(node, ast.Set):
        new_type = TypeVariable()
        for elt in node.elts:
            elt_type = analyse(elt, env, non_generic)
            try:
                unify(new_type, elt_type)
            except InferenceError:
                raise PythranTypeError(
                    "Incompatible set element type `{}` and `{}`".format(
                        new_type, elt_type),
                    node
                )
        return Set(new_type)
    elif isinstance(node, ast.Dict):
        new_key_type = TypeVariable()
        for key in node.keys:
            key_type = analyse(key, env, non_generic)
            try:
                unify(new_key_type, key_type)
            except InferenceError:
                raise PythranTypeError(
                    "Incompatible dict key type `{}` and `{}`".format(
                        new_key_type, key_type),
                    node
                )
        new_value_type = TypeVariable()
        for value in node.values:
            value_type = analyse(value, env, non_generic)
            try:
                unify(new_value_type, value_type)
            except InferenceError:
                raise PythranTypeError(
                    "Incompatible dict value type `{}` and `{}`".format(
                        new_value_type, value_type),
                    node
                )
        return Dict(new_key_type, new_value_type)
    elif isinstance(node, ast.Tuple):
        return Tuple([analyse(elt, env, non_generic) for elt in node.elts])
    elif isinstance(node, ast.Slice):
        def unify_int_or_none(t, name):
            try:
                unify(t, Integer())
            except InferenceError:
                try:
                    unify(t, NoneType)
                except InferenceError:
                    raise PythranTypeError(
                        "Invalid slice {} type `{}`, expecting int or None"
                        .format(name, t)
                    )
        if node.lower:
            lower_type = analyse(node.lower, env, non_generic)
            unify_int_or_none(lower_type, 'lower bound')
        else:
            lower_type = Integer()
        if node.upper:
            upper_type = analyse(node.upper, env, non_generic)
            unify_int_or_none(upper_type, 'upper bound')
        else:
            upper_type = Integer()
        if node.step:
            step_type = analyse(node.step, env, non_generic)
            unify_int_or_none(step_type, 'step')
        else:
            step_type = Integer()
        return Slice
    elif isinstance(node, ast.Subscript):
        new_type = TypeVariable()
        value_type = prune(analyse(node.value, env, non_generic))
        try:
            slice_type = prune(analyse(node.slice, env, non_generic))
        except PythranTypeError as e:
            raise PythranTypeError(e.msg, node)

        if isinstance(node.slice, ast.Tuple):
            nbslice = len(node.slice.elts)
            dtype = TypeVariable()
            try:
                unify(Array(dtype, nbslice), clone(value_type))
            except InferenceError:
                raise PythranTypeError(
                    "Dimension mismatch when slicing `{}`".format(value_type),
                    node)
            return TypeVariable()  # FIXME
        else:
            # handle tuples in a special way
            num = isnum(node.slice)
            if num and is_tuple_type(value_type):
                try:
                    unify(prune(prune(value_type.types[0]).types[0])
                          .types[node.slice.value],
                          new_type)
                    return new_type
                except IndexError:
                    raise PythranTypeError(
                        "Invalid tuple indexing, "
                        "out-of-bound index `{}` for type `{}`".format(
                            node.slice.value,
                            value_type),
                        node)
        try:
            unify(tr(MODULES['operator']['getitem']),
                  Function([value_type, slice_type], new_type))
        except InferenceError:
            raise PythranTypeError(
                "Invalid subscripting of `{}` by `{}`".format(
                    value_type,
                    slice_type),
                node)
        return new_type
        return new_type
    elif isinstance(node, ast.Attribute):
        from pythran.utils import attr_to_path
        obj, path = attr_to_path(node)
        if obj.signature is typing.Any:
            return TypeVariable()
        else:
            return tr(obj)

    # stmt
    elif isinstance(node, ast.Import):
        for alias in node.names:
            if alias.name not in MODULES:
                raise NotImplementedError("unknown module: %s " % alias.name)
            if alias.asname is None:
                target = alias.name
            else:
                target = alias.asname
            env[target] = tr(MODULES[alias.name])
        return env
    elif isinstance(node, ast.ImportFrom):
        if node.module not in MODULES:
            raise NotImplementedError("unknown module: %s" % node.module)
        for alias in node.names:
            if alias.name not in MODULES[node.module]:
                raise NotImplementedError(
                    "unknown function: %s in %s" % (alias.name, node.module))
            if alias.asname is None:
                target = alias.name
            else:
                target = alias.asname
            env[target] = tr(MODULES[node.module][alias.name])
        return env
    elif isinstance(node, ast.FunctionDef):
        ftypes = []
        for i in range(1 + len(node.args.defaults)):
            new_env = env.copy()
            new_non_generic = non_generic.copy()

            # reset return special variables
            new_env.pop('@ret', None)
            new_env.pop('@gen', None)

            hy = HasYield()
            for stmt in node.body:
                hy.visit(stmt)
            new_env['@gen'] = hy.has_yield

            arg_types = []
            istop = len(node.args.args) - i
            for arg in node.args.args[:istop]:
                arg_type = TypeVariable()
                new_env[arg.id] = arg_type
                new_non_generic.add(arg_type)
                arg_types.append(arg_type)
            for arg, expr in zip(node.args.args[istop:],
                                 node.args.defaults[-i:]):
                arg_type = analyse(expr, new_env, new_non_generic)
                new_env[arg.id] = arg_type

            analyse_body(node.body, new_env, new_non_generic)

            result_type = new_env.get('@ret', NoneType)

            if new_env['@gen']:
                result_type = Generator(result_type)

            ftype = Function(arg_types, result_type)
            ftypes.append(ftype)
        if len(ftypes) == 1:
            ftype = ftypes[0]
            env[node.name] = ftype
        else:
            env[node.name] = MultiType(ftypes)
        return env
    elif isinstance(node, ast.Module):
        analyse_body(node.body, env, non_generic)
        return env
    elif isinstance(node, (ast.Pass, ast.Break, ast.Continue)):
        return env
    elif isinstance(node, ast.Expr):
        analyse(node.value, env, non_generic)
        return env
    elif isinstance(node, ast.Delete):
        for target in node.targets:
            if isinstance(target, ast.Name):
                if target.id in env:
                    del env[target.id]
                else:
                    raise PythranTypeError(
                        "Invalid del: unbound identifier `{}`".format(
                            target.id),
                        node)
            else:
                analyse(target, env, non_generic)
        return env
    elif isinstance(node, ast.Print):
        if node.dest is not None:
            analyse(node.dest, env, non_generic)
        for value in node.values:
            analyse(value, env, non_generic)
        return env
    elif isinstance(node, ast.Assign):
        defn_type = analyse(node.value, env, non_generic)
        for target in node.targets:
            target_type = analyse(target, env, non_generic)
            try:
                unify(target_type, defn_type)
            except InferenceError:
                raise PythranTypeError(
                    "Invalid assignment from type `{}` to type `{}`".format(
                        target_type,
                        defn_type),
                    node)
        return env
    elif isinstance(node, ast.AugAssign):
        # FIMXE: not optimal: evaluates type of node.value twice
        fake_target = deepcopy(node.target)
        fake_target.ctx = ast.Load()
        fake_op = ast.BinOp(fake_target, node.op, node.value)
        ast.copy_location(fake_op, node)
        res_type = analyse(fake_op, env, non_generic)
        target_type = analyse(node.target, env, non_generic)

        try:
            unify(target_type, res_type)
        except InferenceError:
            raise PythranTypeError(
                "Invalid update operand for `{}`: `{}` and `{}`".format(
                    symbol_of[type(node.op)],
                    res_type,
                    target_type
                ),
                node
            )
        return env
    elif isinstance(node, ast.Raise):
        return env  # TODO
    elif isinstance(node, ast.Return):
        if env['@gen']:
            return env

        if node.value is None:
            ret_type = NoneType
        else:
            ret_type = analyse(node.value, env, non_generic)
        if '@ret' in env:
            try:
                ret_type = merge_unify(env['@ret'], ret_type)
            except InferenceError:
                raise PythranTypeError(
                    "function may returns with incompatible types "
                    "`{}` and `{}`".format(env['@ret'], ret_type),
                    node
                )

        env['@ret'] = ret_type
        return env
    elif isinstance(node, ast.Yield):
        assert env['@gen']
        assert node.value is not None

        if node.value is None:
            ret_type = NoneType
        else:
            ret_type = analyse(node.value, env, non_generic)
        if '@ret' in env:
            try:
                ret_type = merge_unify(env['@ret'], ret_type)
            except InferenceError:
                raise PythranTypeError(
                    "function may yields incompatible types "
                    "`{}` and `{}`".format(env['@ret'], ret_type),
                    node
                )

        env['@ret'] = ret_type
        return env
    elif isinstance(node, ast.For):
        iter_type = analyse(node.iter, env, non_generic)
        target_type = analyse(node.target, env, non_generic)
        unify(Collection(TypeVariable(), TypeVariable(), TypeVariable(),
                         target_type),
              iter_type)
        analyse_body(node.body, env, non_generic)
        analyse_body(node.orelse, env, non_generic)
        return env
    elif isinstance(node, ast.If):
        test_type = analyse(node.test, env, non_generic)
        unify(Function([test_type], Bool()),
              tr(MODULES['builtins']['bool']))

        body_env = env.copy()
        body_non_generic = non_generic.copy()

        if is_test_is_none(node.test):
            none_id = node.test.left.id
            body_env[none_id] = NoneType
        else:
            none_id = None

        analyse_body(node.body, body_env, body_non_generic)

        orelse_env = env.copy()
        orelse_non_generic = non_generic.copy()

        if none_id:
            if is_option_type(env[none_id]):
                orelse_env[none_id] = prune(env[none_id]).types[0]
            else:
                orelse_env[none_id] = TypeVariable()
        analyse_body(node.orelse, orelse_env, orelse_non_generic)

        for var in body_env:
            if var not in env:
                if var in orelse_env:
                    try:
                        new_type = merge_unify(body_env[var], orelse_env[var])
                    except InferenceError:
                        raise PythranTypeError(
                            "Incompatible types from different branches for "
                            "`{}`: `{}` and `{}`".format(
                                var,
                                body_env[var],
                                orelse_env[var]
                            ),
                            node
                        )
                else:
                    new_type = body_env[var]
                env[var] = new_type

        for var in orelse_env:
            if var not in env:
                # may not be unified by the prev loop if a del occured
                if var in body_env:
                    new_type = merge_unify(orelse_env[var], body_env[var])
                else:
                    new_type = orelse_env[var]
                env[var] = new_type

        if none_id:
            try:
                new_type = merge_unify(body_env[none_id], orelse_env[none_id])
            except InferenceError:
                msg = ("Inconsistent types while merging values of `{}` from "
                       "conditional branches: `{}` and `{}`")
                err = msg.format(none_id,
                                 body_env[none_id],
                                 orelse_env[none_id])
                raise PythranTypeError(err, node)
            env[none_id] = new_type

        return env
    elif isinstance(node, ast.While):
        test_type = analyse(node.test, env, non_generic)
        unify(Function([test_type], Bool()),
              tr(MODULES['builtins']['bool']))

        analyse_body(node.body, env, non_generic)
        analyse_body(node.orelse, env, non_generic)
        return env
    elif isinstance(node, ast.Try):
        analyse_body(node.body, env, non_generic)
        for handler in node.handlers:
            analyse(handler, env, non_generic)
        analyse_body(node.orelse, env, non_generic)
        analyse_body(node.finalbody, env, non_generic)
        return env
    elif isinstance(node, ast.ExceptHandler):
        if(node.name):
            new_type = ExceptionType
            non_generic.add(new_type)
            if node.name.id in env:
                unify(env[node.name.id], new_type)
            else:
                env[node.name.id] = new_type
        analyse_body(node.body, env, non_generic)
        return env
    elif isinstance(node, ast.Assert):
        if node.msg:
            analyse(node.msg, env, non_generic)
        analyse(node.test, env, non_generic)
        return env
    elif isinstance(node, ast.UnaryOp):
        operand_type = analyse(node.operand, env, non_generic)
        return_type = TypeVariable()
        op_type = analyse(node.op, env, non_generic)
        unify(Function([operand_type], return_type), op_type)
        return return_type
    elif isinstance(node, ast.Invert):
        return MultiType([Function([Bool()], Integer()),
                          Function([Integer()], Integer())])
    elif isinstance(node, ast.Not):
        return tr(MODULES['builtins']['bool'])
    elif isinstance(node, ast.BoolOp):
        op_type = analyse(node.op, env, non_generic)
        value_types = [analyse(value, env, non_generic)
                       for value in node.values]

        for value_type in value_types:
            unify(Function([value_type], Bool()),
                  tr(MODULES['builtins']['bool']))

        return_type = TypeVariable()
        prev_type = value_types[0]
        for value_type in value_types[1:]:
            unify(Function([prev_type, value_type], return_type), op_type)
            prev_type = value_type
        return return_type
    elif isinstance(node, (ast.And, ast.Or)):
        x_type = TypeVariable()
        return MultiType([
            Function([x_type, x_type], x_type),
            Function([TypeVariable(), TypeVariable()], TypeVariable()),
        ])

    raise RuntimeError("Unhandled syntax node {0}".format(type(node)))


def get_type(name, env, non_generic):
    """Get the type of identifier name from the type environment env.

    Args:
        name: The identifier name
        env: The type environment mapping from identifier names to types
        non_generic: A set of non-generic TypeVariables

    Raises:
        ParseError: Raised if name is an undefined symbol in the type
            environment.
    """
    if name in env:
        if isinstance(env[name], MultiType):
            return clone(env[name])
        return fresh(env[name], non_generic)
    else:
        print("W: Undefined symbol {0}".format(name))
        return TypeVariable()


def fresh(t, non_generic):
    """Makes a copy of a type expression.

    The type t is copied. The generic variables are duplicated and the
    non_generic variables are shared.

    Args:
        t: A type to be copied.
        non_generic: A set of non-generic TypeVariables
    """

    mappings = {}  # A mapping of TypeVariables to TypeVariables

    def freshrec(tp):
        p = prune(tp)
        if isinstance(p, TypeVariable):
            if is_generic(p, non_generic):
                if p not in mappings:
                    mappings[p] = TypeVariable()
                return mappings[p]
            else:
                return p
        elif isinstance(p, dict):
            return p  # module
        elif isinstance(p, Collection):
            return Collection(*[freshrec(x) for x in p.types])
        elif isinstance(p, Scalar):
            return Scalar([freshrec(x) for x in p.types])
        elif isinstance(p, TypeOperator):
            return TypeOperator(p.name, [freshrec(x) for x in p.types])
        elif isinstance(p, MultiType):
            return MultiType([freshrec(x) for x in p.types])
        else:
            assert False, "missing freshrec case {}".format(type(p))

    return freshrec(t)


def clone(t):
    if isinstance(t, MultiType):
        return MultiType([clone(tp) for tp in t.types])
    else:
        return fresh(t, {})


def unify(t1, t2):
    """Unify the two types t1 and t2.

    Makes the types t1 and t2 the same.

    Args:
        t1: The first type to be made equivalent
        t2: The second type to be be equivalent

    Returns:
        None

    Raises:
        InferenceError: Raised if the types cannot be unified.
    """

    a = prune(t1)
    b = prune(t2)
    if isinstance(a, TypeVariable):
        if a != b:
            if occurs_in_type(a, b):
                raise InferenceError("recursive unification")
            a.instance = b
    elif isinstance(b, TypeVariable):
        unify(b, a)
    elif isinstance(a, TypeOperator) and a.name == 'any':
        return
    elif isinstance(b, TypeOperator) and b.name == 'any':
        return
    elif isinstance(a, TypeOperator) and isinstance(b, TypeOperator):
        if len(a.types) != len(b.types):
            raise InferenceError("Type length differ")
        else:
            if a.name != b.name:
                raise InferenceError("Type name differ")
        try:
            for p, q in zip(a.types, b.types):
                unify(p, q)
        except InferenceError:
            raise
    elif isinstance(a, MultiType) and isinstance(b, MultiType):
        if len(a.types) != len(b.types):
            raise InferenceError("Type lenght differ")
        for p, q in zip(a.types, b.types):
            unify(p, q)
    elif isinstance(b, MultiType):
        return unify(b, a)
    elif isinstance(a, MultiType):
        types = []
        for t in a.types:
            try:
                t_clone = fresh(t, {})
                b_clone = fresh(b, {})
                unify(t_clone, b_clone)
                types.append(t)
            except InferenceError:
                pass
        if types:
            if len(types) == 1:
                unify(clone(types[0]), b)
            else:
                # too many overloads are found,
                # so extract as many information as we can,
                # and leave the remaining over-approximated
                def try_unify(t, ts):
                    if isinstance(t, TypeVariable):
                        return
                    if any(isinstance(tp, TypeVariable) for tp in ts):
                        return
                    if any(len(tp.types) != len(t.types) for tp in ts):
                        return
                    for i, tt in enumerate(t.types):
                        its = [prune(tp.types[i]) for tp in ts]
                        if any(isinstance(it, TypeVariable) for it in its):
                            continue
                        it0 = its[0]
                        it0ntypes = len(it0.types)
                        if all(((it.name == it0.name) and
                                (len(it.types) == it0ntypes))
                               for it in its):
                            ntypes = [TypeVariable() for _ in range(it0ntypes)]
                            new_tt = TypeOperator(it0.name, ntypes)
                            new_tt.__class__ = it0.__class__
                            unify(tt, new_tt)
                            try_unify(prune(tt), [prune(it) for it in its])
                try_unify(b, types)
        else:
            raise InferenceError("No overload")
    else:
        raise RuntimeError("Not unified {} and {}".format(type(a), type(b)))


def merge_unify(t1, t2):
    p1 = prune(t1)
    p2 = prune(t2)
    if is_none(p1) and is_none(p2):
        return p1
    if is_none(p1):
        if is_option_type(p2):
            return p2
        else:
            return OptionType(p2)
    if is_none(p2):
        return merge_unify(p2, p1)
    if is_option_type(p1) and is_option_type(p2):
        unify(p1.types[0], p2.types[0])
        return p1
    if is_option_type(p1):
        unify(p1.types[0], p2)
        return p1
    if is_option_type(p2):
        return merge_unify(p2, p1)
    unify(p1, p2)
    return p1


def prune(t):
    """Returns the currently defining instance of t.

    As a side effect, collapses the list of type instances. The function Prune
    is used whenever a type expression has to be inspected: it will always
    return a type expression which is either an uninstantiated type variable or
    a type operator; i.e. it will skip instantiated variables, and will
    actually prune them from expressions to remove long chains of instantiated
    variables.

    Args:
        t: The type to be pruned

    Returns:
        An uninstantiated TypeVariable or a TypeOperator
    """
    if isinstance(t, TypeVariable):
        if t.instance is not None:
            t.instance = prune(t.instance)
            return t.instance
    return t


def is_generic(v, non_generic):
    """Checks whether a given variable occurs in a list of non-generic variables

    Note that a variables in such a list may be instantiated to a type term,
    in which case the variables contained in the type term are considered
    non-generic.

    Note: Must be called with v pre-pruned

    Args:
        v: The TypeVariable to be tested for genericity
        non_generic: A set of non-generic TypeVariables

    Returns:
        True if v is a generic variable, otherwise False
    """
    return not occurs_in(v, non_generic)


def occurs_in_type(v, type2):
    """Checks whether a type variable occurs in a type expression.

    Note: Must be called with v pre-pruned

    Args:
        v:  The TypeVariable to be tested for
        type2: The type in which to search

    Returns:
        True if v occurs in type2, otherwise False
    """
    pruned_type2 = prune(type2)
    if pruned_type2 == v:
        return True
    elif isinstance(pruned_type2, TypeOperator):
        return occurs_in(v, pruned_type2.types)
    return False


def occurs_in(t, types):
    """Checks whether a types variable occurs in any other types.

    Args:
        t:  The TypeVariable to be tested for
        types: The sequence of types in which to search

    Returns:
        True if t occurs in any of types, otherwise False
    """
    return any(occurs_in_type(t, t2) for t2 in types)


def typecheck(node):
    types = analyse(node, {'builtins': MODULES['builtins']})
    return types
