from collections import namedtuple
import operator
from functools import partial

from llvmlite.llvmpy.core import Constant, Type, Builder

from numba.core import (typing, utils, types, ir, debuginfo, funcdesc,
                        generators, config, ir_utils, cgutils, removerefctpass)
from numba.core.errors import (LoweringError, new_error_context, TypingError,
                               LiteralTypingError, UnsupportedError)
from numba.core.funcdesc import default_mangler
from numba.core.environment import Environment


_VarArgItem = namedtuple("_VarArgItem", ("vararg", "index"))


class BaseLower(object):
    """
    Lower IR to LLVM
    """

    def __init__(self, context, library, fndesc, func_ir, metadata=None):
        self.library = library
        self.fndesc = fndesc
        self.blocks = utils.SortedMap(func_ir.blocks.items())
        self.func_ir = func_ir
        self.call_conv = context.call_conv
        self.generator_info = func_ir.generator_info
        self.metadata = metadata

        # Initialize LLVM
        self.module = self.library.create_ir_module(self.fndesc.unique_name)

        # Python execution environment (will be available to the compiled
        # function).
        self.env = Environment.from_fndesc(self.fndesc)

        # Internal states
        self.blkmap = {}
        self.pending_phis = {}
        self.varmap = {}
        self.firstblk = min(self.blocks.keys())
        self.loc = -1

        # Specializes the target context as seen inside the Lowerer
        # This adds:
        #  - environment: the python execution environment
        self.context = context.subtarget(environment=self.env,
                                         fndesc=self.fndesc)

        # Debuginfo
        dibuildercls = (self.context.DIBuilder
                        if self.context.enable_debuginfo
                        else debuginfo.DummyDIBuilder)

        self.debuginfo = dibuildercls(module=self.module,
                                      filepath=func_ir.loc.filename)

        # Subclass initialization
        self.init()

    def init(self):
        pass

    def init_pyapi(self):
        """
        Init the Python API and Environment Manager for the function being
        lowered.
        """
        if self.pyapi is not None:
            return
        self.pyapi = self.context.get_python_api(self.builder)

        # Store environment argument for later use
        self.env_manager = self.context.get_env_manager(self.builder)
        self.env_body = self.env_manager.env_body
        self.envarg = self.env_manager.env_ptr

    def pre_lower(self):
        """
        Called before lowering all blocks.
        """
        # A given Lower object can be used for several LL functions
        # (for generators) and it's important to use a new API and
        # EnvironmentManager.
        self.pyapi = None
        self.debuginfo.mark_subprogram(function=self.builder.function,
                                       name=self.fndesc.qualname,
                                       loc=self.func_ir.loc)

    def post_lower(self):
        """
        Called after all blocks are lowered
        """
        self.debuginfo.finalize()

    def pre_block(self, block):
        """
        Called before lowering a block.
        """

    def post_block(self, block):
        """
        Called after lowering a block.
        """

    def return_exception(self, exc_class, exc_args=None, loc=None):
        """Propagate exception to the caller.
        """
        self.call_conv.return_user_exc(
            self.builder, exc_class, exc_args,
            loc=loc, func_name=self.func_ir.func_id.func_name,
        )

    def set_exception(self, exc_class, exc_args=None, loc=None):
        """Set exception state in the current function.
        """
        self.call_conv.set_static_user_exc(
            self.builder, exc_class, exc_args,
            loc=loc, func_name=self.func_ir.func_id.func_name,
        )

    def emit_environment_object(self):
        """Emit a pointer to hold the Environment object.
        """
        # Define global for the environment and initialize it to NULL
        envname = self.context.get_env_name(self.fndesc)
        self.context.declare_env_global(self.module, envname)

    def lower(self):
        # Emit the Env into the module
        self.emit_environment_object()
        if self.generator_info is None:
            self.genlower = None
            self.lower_normal_function(self.fndesc)
        else:
            self.genlower = self.GeneratorLower(self)
            self.gentype = self.genlower.gentype

            self.genlower.lower_init_func(self)
            self.genlower.lower_next_func(self)
            if self.gentype.has_finalizer:
                self.genlower.lower_finalize_func(self)

        if config.DUMP_LLVM:
            print(("LLVM DUMP %s" % self.fndesc).center(80, '-'))
            if config.HIGHLIGHT_DUMPS:
                try:
                    from pygments import highlight
                    from pygments.lexers import LlvmLexer as lexer
                    from pygments.formatters import Terminal256Formatter
                    from numba.misc.dump_style import by_colorscheme
                    print(highlight(self.module.__repr__(), lexer(),
                                    Terminal256Formatter(
                                        style=by_colorscheme())))
                except ImportError:
                    msg = "Please install pygments to see highlighted dumps"
                    raise ValueError(msg)
            else:
                print(self.module)
            print('=' * 80)

        # Special optimization to remove NRT on functions that do not need it.
        if self.context.enable_nrt and self.generator_info is None:
            removerefctpass.remove_unnecessary_nrt_usage(self.function,
                                                         context=self.context,
                                                         fndesc=self.fndesc)

        # Run target specific post lowering transformation
        self.context.post_lowering(self.module, self.library)

        # Materialize LLVM Module
        self.library.add_ir_module(self.module)

    def extract_function_arguments(self):
        self.fnargs = self.call_conv.decode_arguments(self.builder,
                                                      self.fndesc.argtypes,
                                                      self.function)
        return self.fnargs

    def lower_normal_function(self, fndesc):
        """
        Lower non-generator *fndesc*.
        """
        self.setup_function(fndesc)

        # Init argument values
        self.extract_function_arguments()
        entry_block_tail = self.lower_function_body()

        # Close tail of entry block
        self.builder.position_at_end(entry_block_tail)
        self.builder.branch(self.blkmap[self.firstblk])

    def lower_function_body(self):
        """
        Lower the current function's body, and return the entry block.
        """
        # Init Python blocks
        for offset in self.blocks:
            bname = "B%s" % offset
            self.blkmap[offset] = self.function.append_basic_block(bname)

        self.pre_lower()
        # pre_lower() may have changed the current basic block
        entry_block_tail = self.builder.basic_block

        self.debug_print("# function begin: {0}".format(
            self.fndesc.unique_name))

        # Lower all blocks
        for offset, block in sorted(self.blocks.items()):
            bb = self.blkmap[offset]
            self.builder.position_at_end(bb)
            self.lower_block(block)
        self.post_lower()
        return entry_block_tail

    def lower_block(self, block):
        """
        Lower the given block.
        """
        self.pre_block(block)
        for inst in block.body:
            self.loc = inst.loc
            defaulterrcls = partial(LoweringError, loc=self.loc)
            with new_error_context('lowering "{inst}" at {loc}', inst=inst,
                                   loc=self.loc, errcls_=defaulterrcls):
                self.lower_inst(inst)
        self.post_block(block)

    def create_cpython_wrapper(self, release_gil=False):
        """
        Create CPython wrapper(s) around this function (or generator).
        """
        if self.genlower:
            self.context.create_cpython_wrapper(self.library,
                                                self.genlower.gendesc,
                                                self.env, self.call_helper,
                                                release_gil=release_gil)
        self.context.create_cpython_wrapper(self.library, self.fndesc,
                                            self.env, self.call_helper,
                                            release_gil=release_gil)

    def create_cfunc_wrapper(self):
        """
        Create C wrapper around this function.
        """
        if self.genlower:
            raise UnsupportedError('generator as a first-class function type')
        self.context.create_cfunc_wrapper(self.library, self.fndesc,
                                          self.env, self.call_helper)

    def setup_function(self, fndesc):
        # Setup function
        self.function = self.context.declare_function(self.module, fndesc)
        self.entry_block = self.function.append_basic_block('entry')
        self.builder = Builder(self.entry_block)
        self.call_helper = self.call_conv.init_call_helper(self.builder)

    def typeof(self, varname):
        return self.fndesc.typemap[varname]

    def debug_print(self, msg):
        if config.DEBUG_JIT:
            self.context.debug_print(self.builder, "DEBUGJIT: {0}".format(msg))


# Dictionary mapping instruction class to its lowering function.
lower_extensions = {}


class Lower(BaseLower):
    GeneratorLower = generators.GeneratorLower

    def pre_block(self, block):
        from numba.core.unsafe import eh

        super(Lower, self).pre_block(block)

        if block == self.firstblk:
            # create slots for all the vars, irrespective of whether they are
            # initialized, SSA will pick this up and warn users about using
            # uninitialized variables. Slots are added as alloca in the first
            # block
            bb = self.blkmap[self.firstblk]
            self.builder.position_at_end(bb)
            all_names = set()
            for block in self.blocks.values():
                for x in block.find_insts(ir.Del):
                    if x.value not in all_names:
                        all_names.add(x.value)
            for name in all_names:
                fetype = self.typeof(name)
                self._alloca_var(name, fetype)

        # Detect if we are in a TRY block by looking for a call to
        # `eh.exception_check`.
        for call in block.find_exprs(op='call'):
            defn = ir_utils.guard(
                ir_utils.get_definition, self.func_ir, call.func,
            )
            if defn is not None and isinstance(defn, ir.Global):
                if defn.value is eh.exception_check:
                    if isinstance(block.terminator, ir.Branch):
                        targetblk = self.blkmap[block.terminator.truebr]
                        # NOTE: This hacks in an attribute for call_conv to
                        #       pick up. This hack is no longer needed when
                        #       all old-style implementations are gone.
                        self.builder._in_try_block = {'target': targetblk}
                        break

    def post_block(self, block):
        # Clean-up
        try:
            del self.builder._in_try_block
        except AttributeError:
            pass

    def lower_inst(self, inst):
        # Set debug location for all subsequent LL instructions
        self.debuginfo.mark_location(self.builder, self.loc)
        self.debug_print(str(inst))
        if isinstance(inst, ir.Assign):
            ty = self.typeof(inst.target.name)
            val = self.lower_assign(ty, inst)
            self.storevar(val, inst.target.name)

        elif isinstance(inst, ir.Branch):
            cond = self.loadvar(inst.cond.name)
            tr = self.blkmap[inst.truebr]
            fl = self.blkmap[inst.falsebr]

            condty = self.typeof(inst.cond.name)
            pred = self.context.cast(self.builder, cond, condty, types.boolean)
            assert pred.type == Type.int(1), ("cond is not i1: %s" % pred.type)
            self.builder.cbranch(pred, tr, fl)

        elif isinstance(inst, ir.Jump):
            target = self.blkmap[inst.target]
            self.builder.branch(target)

        elif isinstance(inst, ir.Return):
            if self.generator_info:
                # StopIteration
                self.genlower.return_from_generator(self)
                return
            val = self.loadvar(inst.value.name)
            oty = self.typeof(inst.value.name)
            ty = self.fndesc.restype
            if isinstance(ty, types.Optional):
                # If returning an optional type
                self.call_conv.return_optional_value(self.builder, ty, oty, val)
                return
            assert ty == oty, (
                "type '{}' does not match return type '{}'".format(oty, ty))
            retval = self.context.get_return_value(self.builder, ty, val)
            self.call_conv.return_value(self.builder, retval)

        elif isinstance(inst, ir.StaticSetItem):
            signature = self.fndesc.calltypes[inst]
            assert signature is not None
            try:
                impl = self.context.get_function('static_setitem', signature)
            except NotImplementedError:
                return self.lower_setitem(inst.target, inst.index_var,
                                          inst.value, signature)
            else:
                target = self.loadvar(inst.target.name)
                value = self.loadvar(inst.value.name)
                valuety = self.typeof(inst.value.name)
                value = self.context.cast(self.builder, value, valuety,
                                          signature.args[2])
                return impl(self.builder, (target, inst.index, value))

        elif isinstance(inst, ir.Print):
            self.lower_print(inst)

        elif isinstance(inst, ir.SetItem):
            signature = self.fndesc.calltypes[inst]
            assert signature is not None
            return self.lower_setitem(inst.target, inst.index, inst.value,
                                      signature)

        elif isinstance(inst, ir.StoreMap):
            signature = self.fndesc.calltypes[inst]
            assert signature is not None
            return self.lower_setitem(inst.dct, inst.key, inst.value, signature)

        elif isinstance(inst, ir.DelItem):
            target = self.loadvar(inst.target.name)
            index = self.loadvar(inst.index.name)

            targetty = self.typeof(inst.target.name)
            indexty = self.typeof(inst.index.name)

            signature = self.fndesc.calltypes[inst]
            assert signature is not None

            op = operator.delitem
            fnop = self.context.typing_context.resolve_value_type(op)
            callsig = fnop.get_call_type(
                self.context.typing_context, signature.args, {},
            )
            impl = self.context.get_function(fnop, callsig)

            assert targetty == signature.args[0]
            index = self.context.cast(self.builder, index, indexty,
                                      signature.args[1])

            return impl(self.builder, (target, index))

        elif isinstance(inst, ir.Del):
            self.delvar(inst.value)

        elif isinstance(inst, ir.SetAttr):
            target = self.loadvar(inst.target.name)
            value = self.loadvar(inst.value.name)
            signature = self.fndesc.calltypes[inst]

            targetty = self.typeof(inst.target.name)
            valuety = self.typeof(inst.value.name)
            assert signature is not None
            assert signature.args[0] == targetty
            impl = self.context.get_setattr(inst.attr, signature)

            # Convert argument to match
            value = self.context.cast(self.builder, value, valuety,
                                      signature.args[1])

            return impl(self.builder, (target, value))

        elif isinstance(inst, ir.StaticRaise):
            self.lower_static_raise(inst)

        elif isinstance(inst, ir.StaticTryRaise):
            self.lower_static_try_raise(inst)

        else:
            for _class, func in lower_extensions.items():
                if isinstance(inst, _class):
                    func(self, inst)
                    return
            raise NotImplementedError(type(inst))

    def lower_setitem(self, target_var, index_var, value_var, signature):
        target = self.loadvar(target_var.name)
        value = self.loadvar(value_var.name)
        index = self.loadvar(index_var.name)

        targetty = self.typeof(target_var.name)
        valuety = self.typeof(value_var.name)
        indexty = self.typeof(index_var.name)

        op = operator.setitem
        fnop = self.context.typing_context.resolve_value_type(op)
        callsig = fnop.get_call_type(
            self.context.typing_context, signature.args, {},
        )
        impl = self.context.get_function(fnop, callsig)

        # Convert argument to match
        if isinstance(targetty, types.Optional):
            target = self.context.cast(self.builder, target, targetty,
                                       targetty.type)
        else:
            ul = types.unliteral
            assert ul(targetty) == ul(signature.args[0])

        index = self.context.cast(self.builder, index, indexty,
                                  signature.args[1])
        value = self.context.cast(self.builder, value, valuety,
                                  signature.args[2])

        return impl(self.builder, (target, index, value))

    def lower_static_raise(self, inst):
        if inst.exc_class is None:
            # Reraise
            self.return_exception(None, loc=self.loc)
        else:
            self.return_exception(inst.exc_class, inst.exc_args, loc=self.loc)

    def lower_static_try_raise(self, inst):
        if inst.exc_class is None:
            # Reraise
            self.set_exception(None, loc=self.loc)
        else:
            self.set_exception(inst.exc_class, inst.exc_args, loc=self.loc)

    def lower_assign(self, ty, inst):
        value = inst.value
        # In nopython mode, closure vars are frozen like globals
        if isinstance(value, (ir.Const, ir.Global, ir.FreeVar)):
            res = self.context.get_constant_generic(self.builder, ty,
                                                    value.value)
            self.incref(ty, res)
            return res

        elif isinstance(value, ir.Expr):
            return self.lower_expr(ty, value)

        elif isinstance(value, ir.Var):
            val = self.loadvar(value.name)
            oty = self.typeof(value.name)
            res = self.context.cast(self.builder, val, oty, ty)
            self.incref(ty, res)
            return res

        elif isinstance(value, ir.Arg):
            # Cast from the argument type to the local variable type
            # (note the "arg.FOO" convention as used in typeinfer)
            argty = self.typeof("arg." + value.name)
            if isinstance(argty, types.Omitted):
                pyval = argty.value
                tyctx = self.context.typing_context
                valty = tyctx.resolve_value_type_prefer_literal(pyval)
                # use the type of the constant value
                const = self.context.get_constant_generic(
                    self.builder, valty, pyval,
                )
                # cast it to the variable type
                res = self.context.cast(self.builder, const, valty, ty)
            else:
                val = self.fnargs[value.index]
                res = self.context.cast(self.builder, val, argty, ty)
            self.incref(ty, res)
            return res

        elif isinstance(value, ir.Yield):
            res = self.lower_yield(ty, value)
            self.incref(ty, res)
            return res

        raise NotImplementedError(type(value), value)

    def lower_yield(self, retty, inst):
        yp = self.generator_info.yield_points[inst.index]
        assert yp.inst is inst
        y = generators.LowerYield(self, yp, yp.live_vars)
        y.lower_yield_suspend()
        # Yield to caller
        val = self.loadvar(inst.value.name)
        typ = self.typeof(inst.value.name)

        # cast the local val to the type yielded
        yret = self.context.cast(self.builder, val, typ,
                                 self.gentype.yield_type)

        # get the return repr of yielded value
        retval = self.context.get_return_value(self.builder, typ, yret)

        # return
        self.call_conv.return_value(self.builder, retval)

        # Resumption point
        y.lower_yield_resume()
        # None is returned by the yield expression
        return self.context.get_constant_generic(self.builder, retty, None)

    def lower_binop(self, resty, expr, op):
        # if op in utils.OPERATORS_TO_BUILTINS:
        # map operator.the_op => the corresponding types.Function()
        # TODO: is this looks dodgy ...
        op = self.context.typing_context.resolve_value_type(op)

        lhs = expr.lhs
        rhs = expr.rhs
        static_lhs = expr.static_lhs
        static_rhs = expr.static_rhs
        lty = self.typeof(lhs.name)
        rty = self.typeof(rhs.name)
        lhs = self.loadvar(lhs.name)
        rhs = self.loadvar(rhs.name)

        # Convert argument to match
        signature = self.fndesc.calltypes[expr]
        lhs = self.context.cast(self.builder, lhs, lty, signature.args[0])
        rhs = self.context.cast(self.builder, rhs, rty, signature.args[1])

        def cast_result(res):
            return self.context.cast(self.builder, res,
                                     signature.return_type, resty)

        # First try with static operands, if known
        def try_static_impl(tys, args):
            if any(a is ir.UNDEFINED for a in args):
                return None
            try:
                if isinstance(op, types.Function):
                    static_sig = op.get_call_type(self.context.typing_context,
                                                  tys, {})
                else:
                    static_sig = typing.signature(signature.return_type, *tys)
            except TypingError:
                return None
            try:
                static_impl = self.context.get_function(op, static_sig)
                return static_impl(self.builder, args)
            except NotImplementedError:
                return None

        res = try_static_impl(
            (_lit_or_omitted(static_lhs), _lit_or_omitted(static_rhs)),
            (static_lhs, static_rhs),
        )
        if res is not None:
            return cast_result(res)

        res = try_static_impl(
            (_lit_or_omitted(static_lhs), rty),
            (static_lhs, rhs),
        )
        if res is not None:
            return cast_result(res)

        res = try_static_impl(
            (lty, _lit_or_omitted(static_rhs)),
            (lhs, static_rhs),
        )
        if res is not None:
            return cast_result(res)

        # Normal implementation for generic arguments

        sig = op.get_call_type(self.context.typing_context, signature.args, {})
        impl = self.context.get_function(op, sig)
        res = impl(self.builder, (lhs, rhs))
        return cast_result(res)

    def lower_getitem(self, resty, expr, value, index, signature):
        baseval = self.loadvar(value.name)
        indexval = self.loadvar(index.name)
        # Get implementation of getitem
        op = operator.getitem
        fnop = self.context.typing_context.resolve_value_type(op)
        callsig = fnop.get_call_type(
            self.context.typing_context, signature.args, {},
        )
        impl = self.context.get_function(fnop, callsig)

        argvals = (baseval, indexval)
        argtyps = (self.typeof(value.name),
                   self.typeof(index.name))
        castvals = [self.context.cast(self.builder, av, at, ft)
                    for av, at, ft in zip(argvals, argtyps,
                                          signature.args)]
        res = impl(self.builder, castvals)
        return self.context.cast(self.builder, res,
                                 signature.return_type,
                                 resty)

    def _cast_var(self, var, ty):
        """
        Cast a Numba IR variable to the given Numba type, returning a
        low-level value.
        """
        if isinstance(var, _VarArgItem):
            varty = self.typeof(var.vararg.name)[var.index]
            val = self.builder.extract_value(self.loadvar(var.vararg.name),
                                             var.index)
        else:
            varty = self.typeof(var.name)
            val = self.loadvar(var.name)
        return self.context.cast(self.builder, val, varty, ty)

    def fold_call_args(self, fnty, signature, pos_args, vararg, kw_args):
        if vararg:
            # Inject *args from function call
            # The lowering will be done in _cast_var() above.
            tp_vararg = self.typeof(vararg.name)
            assert isinstance(tp_vararg, types.BaseTuple)
            pos_args = pos_args + [_VarArgItem(vararg, i)
                                   for i in range(len(tp_vararg))]

        # Fold keyword arguments and resolve default argument values
        pysig = signature.pysig
        if pysig is None:
            if kw_args:
                raise NotImplementedError("unsupported keyword arguments "
                                          "when calling %s" % (fnty,))
            argvals = [self._cast_var(var, sigty)
                       for var, sigty in zip(pos_args, signature.args)]
        else:
            def normal_handler(index, param, var):
                return self._cast_var(var, signature.args[index])

            def default_handler(index, param, default):
                return self.context.get_constant_generic(
                    self.builder, signature.args[index], default)

            def stararg_handler(index, param, vars):
                stararg_ty = signature.args[index]
                assert isinstance(stararg_ty, types.BaseTuple), stararg_ty
                values = [self._cast_var(var, sigty)
                          for var, sigty in zip(vars, stararg_ty)]
                return cgutils.make_anonymous_struct(self.builder, values)

            argvals = typing.fold_arguments(pysig,
                                            pos_args, dict(kw_args),
                                            normal_handler,
                                            default_handler,
                                            stararg_handler)
        return argvals

    def lower_print(self, inst):
        """
        Lower a ir.Print()
        """
        # We handle this, as far as possible, as a normal call to built-in
        # print().  This will make it easy to undo the special ir.Print
        # rewrite when it becomes unnecessary (e.g. when we have native
        # strings).
        sig = self.fndesc.calltypes[inst]
        assert sig.return_type == types.none
        fnty = self.context.typing_context.resolve_value_type(print)

        # Fix the call signature to inject any constant-inferred
        # string argument
        pos_tys = list(sig.args)
        pos_args = list(inst.args)
        for i in range(len(pos_args)):
            if i in inst.consts:
                pyval = inst.consts[i]
                if isinstance(pyval, str):
                    pos_tys[i] = types.literal(pyval)

        fixed_sig = typing.signature(sig.return_type, *pos_tys)
        fixed_sig = fixed_sig.replace(pysig=sig.pysig)

        argvals = self.fold_call_args(fnty, sig, pos_args, inst.vararg, {})
        impl = self.context.get_function(print, fixed_sig)
        impl(self.builder, argvals)

    def lower_call(self, resty, expr):
        signature = self.fndesc.calltypes[expr]
        self.debug_print("# lower_call: expr = {0}".format(expr))
        if isinstance(signature.return_type, types.Phantom):
            return self.context.get_dummy_value()

        fnty = self.typeof(expr.func.name)

        if isinstance(fnty, types.ObjModeDispatcher):
            res = self._lower_call_ObjModeDispatcher(fnty, expr, signature)

        elif isinstance(fnty, types.ExternalFunction):
            res = self._lower_call_ExternalFunction(fnty, expr, signature)

        elif isinstance(fnty, types.ExternalFunctionPointer):
            res = self._lower_call_ExternalFunctionPointer(
                fnty, expr, signature)

        elif isinstance(fnty, types.RecursiveCall):
            res = self._lower_call_RecursiveCall(fnty, expr, signature)

        elif isinstance(fnty, types.FunctionType):
            res = self._lower_call_FunctionType(fnty, expr, signature)

        else:
            res = self._lower_call_normal(fnty, expr, signature)

        # If lowering the call returned None, interpret that as returning dummy
        # value if the return type of the function is void, otherwise there is
        # a problem
        if res is None:
            if signature.return_type == types.void:
                res = self.context.get_dummy_value()
            else:
                raise LoweringError(
                    msg="non-void function returns None from implementation",
                    loc=self.loc
                )

        return self.context.cast(self.builder, res, signature.return_type,
                                 resty)

    def _lower_call_ObjModeDispatcher(self, fnty, expr, signature):
        from numba.core.pythonapi import ObjModeUtils

        self.init_pyapi()
        # Acquire the GIL
        gil_state = self.pyapi.gil_ensure()
        # Fix types
        argnames = [a.name for a in expr.args]
        argtypes = [self.typeof(a) for a in argnames]
        argvalues = [self.loadvar(a) for a in argnames]
        for v, ty in zip(argvalues, argtypes):
            # Because .from_native_value steal the reference
            self.incref(ty, v)

        argobjs = [self.pyapi.from_native_value(atyp, aval,
                                                self.env_manager)
                   for atyp, aval in zip(argtypes, argvalues)]

        # Load objmode dispatcher
        callee = ObjModeUtils(self.pyapi).load_dispatcher(fnty, argtypes)
        # Make Call
        ret_obj = self.pyapi.call_function_objargs(callee, argobjs)
        has_exception = cgutils.is_null(self.builder, ret_obj)
        with self. builder.if_else(has_exception) as (then, orelse):
            # Handles exception
            # This branch must exit the function
            with then:
                # Clean arg
                for obj in argobjs:
                    self.pyapi.decref(obj)

                # Release the GIL
                self.pyapi.gil_release(gil_state)

                # Return and signal exception
                self.call_conv.return_exc(self.builder)

            # Handles normal return
            with orelse:
                # Fix output value
                native = self.pyapi.to_native_value(
                    fnty.dispatcher.output_types,
                    ret_obj,
                )
                output = native.value

                # Release objs
                self.pyapi.decref(ret_obj)
                for obj in argobjs:
                    self.pyapi.decref(obj)

                # cleanup output
                if callable(native.cleanup):
                    native.cleanup()

                # Release the GIL
                self.pyapi.gil_release(gil_state)

                # Error during unboxing
                with self.builder.if_then(native.is_error):
                    self.call_conv.return_exc(self.builder)

                return output

    def _lower_call_ExternalFunction(self, fnty, expr, signature):
        # Handle a named external function
        self.debug_print("# external function")
        argvals = self.fold_call_args(
            fnty, signature, expr.args, expr.vararg, expr.kws,
        )
        fndesc = funcdesc.ExternalFunctionDescriptor(
            fnty.symbol, fnty.sig.return_type, fnty.sig.args)
        func = self.context.declare_external_function(
            self.builder.module, fndesc)
        return self.context.call_external_function(
            self.builder, func, fndesc.argtypes, argvals,
        )

    def _lower_call_ExternalFunctionPointer(self, fnty, expr, signature):
        # Handle a C function pointer
        self.debug_print("# calling external function pointer")
        argvals = self.fold_call_args(
            fnty, signature, expr.args, expr.vararg, expr.kws,
        )
        pointer = self.loadvar(expr.func.name)
        # If the external function pointer uses libpython
        if fnty.requires_gil:
            self.init_pyapi()
            # Acquire the GIL
            gil_state = self.pyapi.gil_ensure()
            # Make PyObjects
            newargvals = []
            pyvals = []
            for exptyp, gottyp, aval in zip(fnty.sig.args, signature.args,
                                            argvals):
                # Adjust argument values to pyobjects
                if exptyp == types.ffi_forced_object:
                    self.incref(gottyp, aval)
                    obj = self.pyapi.from_native_value(
                        gottyp, aval, self.env_manager,
                    )
                    newargvals.append(obj)
                    pyvals.append(obj)
                else:
                    newargvals.append(aval)

            # Call external function
            res = self.context.call_function_pointer(
                self.builder, pointer, newargvals, fnty.cconv,
            )
            # Release PyObjects
            for obj in pyvals:
                self.pyapi.decref(obj)

            # Release the GIL
            self.pyapi.gil_release(gil_state)
        # If the external function pointer does NOT use libpython
        else:
            res = self.context.call_function_pointer(
                self.builder, pointer, argvals, fnty.cconv,
            )
        return res

    def _lower_call_RecursiveCall(self, fnty, expr, signature):
        # Recursive call
        argvals = self.fold_call_args(
            fnty, signature, expr.args, expr.vararg, expr.kws,
        )
        qualprefix = fnty.overloads[signature.args]
        mangler = self.context.mangler or default_mangler
        mangled_name = mangler(qualprefix, signature.args)
        # special case self recursion
        if self.builder.function.name.startswith(mangled_name):
            res = self.context.call_internal(
                self.builder, self.fndesc, signature, argvals,
            )
        else:
            res = self.context.call_unresolved(
                self.builder, mangled_name, signature, argvals,
            )
        return res

    def _lower_call_FunctionType(self, fnty, expr, signature):
        self.debug_print("# calling first-class function type")
        sig = types.unliteral(signature)
        if not fnty.check_signature(signature):
            # value dependent polymorphism?
            raise UnsupportedError(
                f'mismatch of function types:'
                f' expected {fnty} but got {types.FunctionType(sig)}')
        ftype = fnty.ftype
        argvals = self.fold_call_args(
            fnty, sig, expr.args, expr.vararg, expr.kws,
        )
        func_ptr = self.__get_function_pointer(ftype, expr.func.name, sig=sig)
        res = self.builder.call(func_ptr, argvals, cconv=fnty.cconv)
        return res

    def __get_function_pointer(self, ftype, fname, sig=None):
        from numba.experimental.function_type import lower_get_wrapper_address

        llty = self.context.get_value_type(ftype)
        fstruct = self.loadvar(fname)
        addr = self.builder.extract_value(fstruct, 0,
                                          name='addr_of_%s' % (fname))

        fptr = cgutils.alloca_once(self.builder, llty,
                                   name="fptr_of_%s" % (fname))
        with self.builder.if_else(
                cgutils.is_null(self.builder, addr),
                likely=False) as (then, orelse):
            with then:
                self.init_pyapi()
                # Acquire the GIL
                gil_state = self.pyapi.gil_ensure()
                pyaddr = self.builder.extract_value(
                    fstruct, 1,
                    name='pyaddr_of_%s' % (fname))
                # try to recover the function address, see
                # test_zero_address BadToGood example in
                # test_function_type.py
                addr1 = lower_get_wrapper_address(
                    self.context, self.builder, pyaddr, sig,
                    failure_mode='ignore')
                with self.builder.if_then(
                        cgutils.is_null(self.builder, addr1), likely=False):
                    self.return_exception(
                        RuntimeError,
                        exc_args=(f"{ftype} function address is null",),
                        loc=self.loc)
                addr2 = self.pyapi.long_as_voidptr(addr1)
                self.builder.store(self.builder.bitcast(addr2, llty), fptr)
                self.pyapi.decref(addr1)
                self.pyapi.gil_release(gil_state)
            with orelse:
                self.builder.store(self.builder.bitcast(addr, llty), fptr)
        return self.builder.load(fptr)

    def _lower_call_normal(self, fnty, expr, signature):
        # Normal function resolution
        self.debug_print("# calling normal function: {0}".format(fnty))
        self.debug_print("# signature: {0}".format(signature))
        if isinstance(fnty, types.ObjModeDispatcher):
            argvals = expr.func.args
        else:
            argvals = self.fold_call_args(
                fnty, signature, expr.args, expr.vararg, expr.kws,
            )
        impl = self.context.get_function(fnty, signature)
        if signature.recvr:
            # The "self" object is passed as the function object
            # for bounded function
            the_self = self.loadvar(expr.func.name)
            # Prepend the self reference
            argvals = [the_self] + list(argvals)

        res = impl(self.builder, argvals, self.loc)
        return res

    def lower_expr(self, resty, expr):
        if expr.op == 'binop':
            return self.lower_binop(resty, expr, expr.fn)
        elif expr.op == 'inplace_binop':
            lty = self.typeof(expr.lhs.name)
            if lty.mutable:
                return self.lower_binop(resty, expr, expr.fn)
            else:
                # inplace operators on non-mutable types reuse the same
                # definition as the corresponding copying operators.)
                return self.lower_binop(resty, expr, expr.immutable_fn)
        elif expr.op == 'unary':
            val = self.loadvar(expr.value.name)
            typ = self.typeof(expr.value.name)
            func_ty = self.context.typing_context.resolve_value_type(expr.fn)
            # Get function
            signature = self.fndesc.calltypes[expr]
            impl = self.context.get_function(func_ty, signature)
            # Convert argument to match
            val = self.context.cast(self.builder, val, typ, signature.args[0])
            res = impl(self.builder, [val])
            res = self.context.cast(self.builder, res,
                                    signature.return_type, resty)
            return res

        elif expr.op == 'call':
            res = self.lower_call(resty, expr)
            return res

        elif expr.op == 'pair_first':
            val = self.loadvar(expr.value.name)
            ty = self.typeof(expr.value.name)
            res = self.context.pair_first(self.builder, val, ty)
            self.incref(resty, res)
            return res

        elif expr.op == 'pair_second':
            val = self.loadvar(expr.value.name)
            ty = self.typeof(expr.value.name)
            res = self.context.pair_second(self.builder, val, ty)
            self.incref(resty, res)
            return res

        elif expr.op in ('getiter', 'iternext'):
            val = self.loadvar(expr.value.name)
            ty = self.typeof(expr.value.name)
            signature = self.fndesc.calltypes[expr]
            impl = self.context.get_function(expr.op, signature)
            [fty] = signature.args
            castval = self.context.cast(self.builder, val, ty, fty)
            res = impl(self.builder, (castval,))
            res = self.context.cast(self.builder, res, signature.return_type,
                                    resty)
            return res

        elif expr.op == 'exhaust_iter':
            val = self.loadvar(expr.value.name)
            ty = self.typeof(expr.value.name)
            # Unpack optional
            if isinstance(ty, types.Optional):
                val = self.context.cast(self.builder, val, ty, ty.type)
                ty = ty.type

            # If we have a tuple, we needn't do anything
            # (and we can't iterate over the heterogeneous ones).
            if isinstance(ty, types.BaseTuple):
                assert ty == resty
                self.incref(ty, val)
                return val

            itemty = ty.iterator_type.yield_type
            tup = self.context.get_constant_undef(resty)
            pairty = types.Pair(itemty, types.boolean)
            getiter_sig = typing.signature(ty.iterator_type, ty)
            getiter_impl = self.context.get_function('getiter',
                                                     getiter_sig)
            iternext_sig = typing.signature(pairty, ty.iterator_type)
            iternext_impl = self.context.get_function('iternext',
                                                      iternext_sig)
            iterobj = getiter_impl(self.builder, (val,))
            # We call iternext() as many times as desired (`expr.count`).
            for i in range(expr.count):
                pair = iternext_impl(self.builder, (iterobj,))
                is_valid = self.context.pair_second(self.builder,
                                                    pair, pairty)
                with cgutils.if_unlikely(self.builder,
                                         self.builder.not_(is_valid)):
                    self.return_exception(ValueError, loc=self.loc)
                item = self.context.pair_first(self.builder,
                                               pair, pairty)
                tup = self.builder.insert_value(tup, item, i)

            # Call iternext() once more to check that the iterator
            # is exhausted.
            pair = iternext_impl(self.builder, (iterobj,))
            is_valid = self.context.pair_second(self.builder,
                                                pair, pairty)
            with cgutils.if_unlikely(self.builder, is_valid):
                self.return_exception(ValueError, loc=self.loc)

            self.decref(ty.iterator_type, iterobj)
            return tup

        elif expr.op == "getattr":
            val = self.loadvar(expr.value.name)
            ty = self.typeof(expr.value.name)

            if isinstance(resty, types.BoundFunction):
                # if we are getting out a method, assume we have typed this
                # properly and just build a bound function object
                casted = self.context.cast(self.builder, val, ty, resty.this)
                res = self.context.get_bound_function(self.builder, casted,
                                                      resty.this)
                self.incref(resty, res)
                return res
            else:
                impl = self.context.get_getattr(ty, expr.attr)
                attrty = self.context.typing_context.resolve_getattr(ty,
                                                                     expr.attr)

                if impl is None:
                    # ignore the attribute
                    return self.context.get_dummy_value()
                else:
                    res = impl(self.context, self.builder, ty, val, expr.attr)

                # Cast the attribute type to the expected output type
                res = self.context.cast(self.builder, res, attrty, resty)
                return res

        elif expr.op == "static_getitem":
            signature = typing.signature(
                resty,
                self.typeof(expr.value.name),
                _lit_or_omitted(expr.index),
            )
            try:
                # Both get_function() and the returned implementation can
                # raise NotImplementedError if the types aren't supported
                impl = self.context.get_function("static_getitem", signature)
                return impl(self.builder,
                            (self.loadvar(expr.value.name), expr.index))
            except NotImplementedError:
                if expr.index_var is None:
                    raise
                # Fall back on the generic getitem() implementation
                # for this type.
                signature = self.fndesc.calltypes[expr]
                return self.lower_getitem(resty, expr, expr.value,
                                          expr.index_var, signature)
        elif expr.op == "typed_getitem":
            signature = typing.signature(
                resty,
                self.typeof(expr.value.name),
                self.typeof(expr.index.name),
            )
            impl = self.context.get_function("typed_getitem", signature)
            return impl(self.builder, (self.loadvar(expr.value.name),
                        self.loadvar(expr.index.name)))
        elif expr.op == "getitem":
            signature = self.fndesc.calltypes[expr]
            return self.lower_getitem(resty, expr, expr.value, expr.index,
                                      signature)

        elif expr.op == "build_tuple":
            itemvals = [self.loadvar(i.name) for i in expr.items]
            itemtys = [self.typeof(i.name) for i in expr.items]
            castvals = [self.context.cast(self.builder, val, fromty, toty)
                        for val, toty, fromty in zip(itemvals, resty, itemtys)]
            tup = self.context.make_tuple(self.builder, resty, castvals)
            self.incref(resty, tup)
            return tup

        elif expr.op == "build_list":
            itemvals = [self.loadvar(i.name) for i in expr.items]
            itemtys = [self.typeof(i.name) for i in expr.items]
            if isinstance(resty, types.LiteralList):
                castvals = [self.context.cast(self.builder, val, fromty, toty)
                            for val, toty, fromty in zip(itemvals, resty.types,
                                                         itemtys)]
                tup = self.context.make_tuple(self.builder,
                                              types.Tuple(resty.types),
                                              castvals)
                self.incref(resty, tup)
                return tup
            else:
                castvals = [self.context.cast(self.builder, val, fromty,
                                              resty.dtype)
                            for val, fromty in zip(itemvals, itemtys)]
                return self.context.build_list(self.builder, resty, castvals)

        elif expr.op == "build_set":
            # Insert in reverse order, as Python does
            items = expr.items[::-1]
            itemvals = [self.loadvar(i.name) for i in items]
            itemtys = [self.typeof(i.name) for i in items]
            castvals = [self.context.cast(self.builder, val, fromty,
                                          resty.dtype)
                        for val, fromty in zip(itemvals, itemtys)]
            return self.context.build_set(self.builder, resty, castvals)

        elif expr.op == "build_map":
            items = expr.items
            keys, values = [], []
            key_types, value_types = [], []
            for k, v in items:
                key = self.loadvar(k.name)
                keytype = self.typeof(k.name)
                val = self.loadvar(v.name)
                valtype = self.typeof(v.name)
                keys.append(key)
                values.append(val)
                key_types.append(keytype)
                value_types.append(valtype)
            return self.context.build_map(self.builder, resty,
                                          list(zip(key_types, value_types)),
                                          list(zip(keys, values)))

        elif expr.op == "cast":
            val = self.loadvar(expr.value.name)
            ty = self.typeof(expr.value.name)
            castval = self.context.cast(self.builder, val, ty, resty)
            self.incref(resty, castval)
            return castval

        elif expr.op == "phi":
            raise LoweringError("PHI not stripped")

        elif expr.op == 'null':
            return self.context.get_constant_null(resty)

        elif expr.op in self.context.special_ops:
            res = self.context.special_ops[expr.op](self, expr)
            return res

        raise NotImplementedError(expr)

    def _alloca_var(self, name, fetype):
        """
        Ensure the given variable has an allocated stack slot.
        """
        if name not in self.varmap:
            # If not already defined, allocate it
            llty = self.context.get_value_type(fetype)
            ptr = self.alloca_lltype(name, llty)
            # Remember the pointer
            self.varmap[name] = ptr

    def getvar(self, name):
        """
        Get a pointer to the given variable's slot.
        """
        return self.varmap[name]

    def loadvar(self, name):
        """
        Load the given variable's value.
        """
        ptr = self.getvar(name)
        return self.builder.load(ptr)

    def storevar(self, value, name):
        """
        Store the value into the given variable.
        """
        fetype = self.typeof(name)
        # Define if not already
        self._alloca_var(name, fetype)

        # Clean up existing value stored in the variable
        old = self.loadvar(name)
        self.decref(fetype, old)

        # Store variable
        ptr = self.getvar(name)
        if value.type != ptr.type.pointee:
            msg = ("Storing {value.type} to ptr of {ptr.type.pointee} "
                   "('{name}'). FE type {fetype}").format(value=value,
                                                          ptr=ptr,
                                                          fetype=fetype,
                                                          name=name)
            raise AssertionError(msg)

        self.builder.store(value, ptr)

    def delvar(self, name):
        """
        Delete the given variable.
        """
        fetype = self.typeof(name)

        # Define if not already (may happen if the variable is deleted
        # at the beginning of a loop, but only set later in the loop)
        self._alloca_var(name, fetype)

        ptr = self.getvar(name)
        self.decref(fetype, self.builder.load(ptr))
        # Zero-fill variable to avoid double frees on subsequent dels
        self.builder.store(Constant.null(ptr.type.pointee), ptr)

    def alloca(self, name, type):
        lltype = self.context.get_value_type(type)
        return self.alloca_lltype(name, lltype)

    def alloca_lltype(self, name, lltype):
        # Is user variable?
        is_uservar = not name.startswith('$')
        # Allocate space for variable
        aptr = cgutils.alloca_once(self.builder, lltype,
                                   name=name, zfill=False)
        if is_uservar:
            # Emit debug info for user variable
            sizeof = self.context.get_abi_sizeof(lltype)
            self.debuginfo.mark_variable(self.builder, aptr, name=name,
                                         lltype=lltype, size=sizeof,
                                         loc=self.loc)
        return aptr

    def incref(self, typ, val):
        if not self.context.enable_nrt:
            return

        self.context.nrt.incref(self.builder, typ, val)

    def decref(self, typ, val):
        if not self.context.enable_nrt:
            return

        self.context.nrt.decref(self.builder, typ, val)


def _lit_or_omitted(value):
    """Returns a Literal instance if the type of value is supported;
    otherwise, return `Omitted(value)`.
    """
    try:
        return types.literal(value)
    except LiteralTypingError:
        return types.Omitted(value)
