from collections import defaultdict, namedtuple
from contextlib import contextmanager
from copy import deepcopy, copy
import warnings

from numba.core.compiler_machinery import (FunctionPass, AnalysisPass,
                                           register_pass)
from numba.core import (errors, types, ir, bytecode, postproc, rewrites, config,
                        transforms)
from numba.misc.special import literal_unroll
from numba.core.analysis import (dead_branch_prune, rewrite_semantic_constants,
                                 find_literally_calls, compute_cfg_from_blocks,
                                 compute_use_defs)
from numba.core.ir_utils import (guard, resolve_func_from_module, simplify_CFG,
                                 GuardException, convert_code_obj_to_function,
                                 mk_unique_var, build_definitions,
                                 replace_var_names, get_name_var_table,
                                 compile_to_numba_ir, get_definition,
                                 find_max_label, rename_labels,
                                 transfer_scope, fixup_var_define_in_scope,
                                 )
from numba.core.ssa import reconstruct_ssa
from numba.core import interpreter


@contextmanager
def fallback_context(state, msg):
    """
    Wraps code that would signal a fallback to object mode
    """
    try:
        yield
    except Exception as e:
        if not state.status.can_fallback:
            raise
        else:
            # Clear all references attached to the traceback
            e = e.with_traceback(None)
            # this emits a warning containing the error message body in the
            # case of fallback from npm to objmode
            loop_lift = '' if state.flags.enable_looplift else 'OUT'
            msg_rewrite = ("\nCompilation is falling back to object mode "
                           "WITH%s looplifting enabled because %s"
                           % (loop_lift, msg))
            warnings.warn_explicit('%s due to: %s' % (msg_rewrite, e),
                                   errors.NumbaWarning,
                                   state.func_id.filename,
                                   state.func_id.firstlineno)
            raise


@register_pass(mutates_CFG=True, analysis_only=False)
class ExtractByteCode(FunctionPass):
    _name = "extract_bytecode"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        """
        Extract bytecode from function
        """
        func_id = state['func_id']
        bc = bytecode.ByteCode(func_id)
        if config.DUMP_BYTECODE:
            print(bc.dump())

        state['bc'] = bc
        return True


@register_pass(mutates_CFG=True, analysis_only=False)
class TranslateByteCode(FunctionPass):
    _name = "translate_bytecode"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        """
        Analyze bytecode and translating to Numba IR
        """
        func_id = state['func_id']
        bc = state['bc']
        interp = interpreter.Interpreter(func_id)
        func_ir = interp.interpret(bc)
        state["func_ir"] = func_ir
        return True


@register_pass(mutates_CFG=True, analysis_only=False)
class FixupArgs(FunctionPass):
    _name = "fixup_args"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        state['nargs'] = state['func_ir'].arg_count
        if not state['args'] and state['flags'].force_pyobject:
            # Allow an empty argument types specification when object mode
            # is explicitly requested.
            state['args'] = (types.pyobject,) * state['nargs']
        elif len(state['args']) != state['nargs']:
            raise TypeError("Signature mismatch: %d argument types given, "
                            "but function takes %d arguments"
                            % (len(state['args']), state['nargs']))
        return True


@register_pass(mutates_CFG=True, analysis_only=False)
class IRProcessing(FunctionPass):
    _name = "ir_processing"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        func_ir = state['func_ir']
        post_proc = postproc.PostProcessor(func_ir)
        post_proc.run()

        if config.DEBUG or config.DUMP_IR:
            name = func_ir.func_id.func_qualname
            print(("IR DUMP: %s" % name).center(80, "-"))
            func_ir.dump()
            if func_ir.is_generator:
                print(("GENERATOR INFO: %s" % name).center(80, "-"))
                func_ir.dump_generator_info()
        return True


@register_pass(mutates_CFG=True, analysis_only=False)
class RewriteSemanticConstants(FunctionPass):
    _name = "rewrite_semantic_constants"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        """
        This prunes dead branches, a dead branch is one which is derivable as
        not taken at compile time purely based on const/literal evaluation.
        """
        assert state.func_ir
        msg = ('Internal error in pre-inference dead branch pruning '
               'pass encountered during compilation of '
               'function "%s"' % (state.func_id.func_name,))
        with fallback_context(state, msg):
            rewrite_semantic_constants(state.func_ir, state.args)

        return True


@register_pass(mutates_CFG=True, analysis_only=False)
class DeadBranchPrune(FunctionPass):
    _name = "dead_branch_prune"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        """
        This prunes dead branches, a dead branch is one which is derivable as
        not taken at compile time purely based on const/literal evaluation.
        """

        # purely for demonstration purposes, obtain the analysis from a pass
        # declare as a required dependent
        semantic_const_analysis = self.get_analysis(type(self))  # noqa

        assert state.func_ir
        msg = ('Internal error in pre-inference dead branch pruning '
               'pass encountered during compilation of '
               'function "%s"' % (state.func_id.func_name,))
        with fallback_context(state, msg):
            dead_branch_prune(state.func_ir, state.args)

        return True

    def get_analysis_usage(self, AU):
        AU.add_required(RewriteSemanticConstants)


@register_pass(mutates_CFG=True, analysis_only=False)
class InlineClosureLikes(FunctionPass):
    _name = "inline_closure_likes"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        # Ensure we have an IR and type information.
        assert state.func_ir

        # if the return type is a pyobject, there's no type info available and
        # no ability to resolve certain typed function calls in the array
        # inlining code, use this variable to indicate
        typed_pass = not isinstance(state.return_type, types.misc.PyObject)
        from numba.core.inline_closurecall import InlineClosureCallPass
        inline_pass = InlineClosureCallPass(
            state.func_ir,
            state.flags.auto_parallel,
            state.parfor_diagnostics.replaced_fns,
            typed_pass)
        inline_pass.run()

        # Remove all Dels, and re-run postproc
        post_proc = postproc.PostProcessor(state.func_ir)
        post_proc.run()

        fixup_var_define_in_scope(state.func_ir.blocks)

        if config.DEBUG or config.DUMP_IR:
            name = state.func_ir.func_id.func_qualname
            print(("IR DUMP: %s" % name).center(80, "-"))
            state.func_ir.dump()
        return True


@register_pass(mutates_CFG=True, analysis_only=False)
class GenericRewrites(FunctionPass):
    _name = "generic_rewrites"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        """
        Perform any intermediate representation rewrites before type
        inference.
        """
        assert state.func_ir
        msg = ('Internal error in pre-inference rewriting '
               'pass encountered during compilation of '
               'function "%s"' % (state.func_id.func_name,))
        with fallback_context(state, msg):
            rewrites.rewrite_registry.apply('before-inference', state)
        return True


@register_pass(mutates_CFG=True, analysis_only=False)
class WithLifting(FunctionPass):
    _name = "with_lifting"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        """
        Extract with-contexts
        """
        main, withs = transforms.with_lifting(
            func_ir=state.func_ir,
            typingctx=state.typingctx,
            targetctx=state.targetctx,
            flags=state.flags,
            locals=state.locals,
        )
        if withs:
            from numba.core.compiler import compile_ir, _EarlyPipelineCompletion
            cres = compile_ir(state.typingctx, state.targetctx, main,
                              state.args, state.return_type,
                              state.flags, state.locals,
                              lifted=tuple(withs), lifted_from=None,
                              pipeline_class=type(state.pipeline))
            raise _EarlyPipelineCompletion(cres)
        return True


@register_pass(mutates_CFG=True, analysis_only=False)
class InlineInlinables(FunctionPass):
    """
    This pass will inline a function wrapped by the numba.jit decorator directly
    into the site of its call depending on the value set in the 'inline' kwarg
    to the decorator.

    This is an untyped pass. CFG simplification is performed at the end of the
    pass but no block level clean up is performed on the mutated IR (typing
    information is not available to do so).
    """
    _name = "inline_inlinables"
    _DEBUG = False

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        """Run inlining of inlinables
        """
        if self._DEBUG:
            print('before inline'.center(80, '-'))
            print(state.func_ir.dump())
            print(''.center(80, '-'))

        from numba.core.inline_closurecall import (InlineWorker,
                                                   callee_ir_validator)
        inline_worker = InlineWorker(state.typingctx,
                                     state.targetctx,
                                     state.locals,
                                     state.pipeline,
                                     state.flags,
                                     validator=callee_ir_validator)

        modified = False
        # use a work list, look for call sites via `ir.Expr.op == call` and
        # then pass these to `self._do_work` to make decisions about inlining.
        work_list = list(state.func_ir.blocks.items())
        while work_list:
            label, block = work_list.pop()
            for i, instr in enumerate(block.body):
                if isinstance(instr, ir.Assign):
                    expr = instr.value
                    if isinstance(expr, ir.Expr) and expr.op == 'call':
                        if guard(self._do_work, state, work_list, block, i,
                                 expr, inline_worker):
                            modified = True
                            break  # because block structure changed

        if modified:
            # clean up unconditional branches that appear due to inlined
            # functions introducing blocks
            cfg = compute_cfg_from_blocks(state.func_ir.blocks)
            for dead in cfg.dead_nodes():
                del state.func_ir.blocks[dead]
            post_proc = postproc.PostProcessor(state.func_ir)
            post_proc.run()
            state.func_ir.blocks = simplify_CFG(state.func_ir.blocks)

        if self._DEBUG:
            print('after inline'.center(80, '-'))
            print(state.func_ir.dump())
            print(''.center(80, '-'))
        return True

    def _do_work(self, state, work_list, block, i, expr, inline_worker):
        from numba.core.compiler import run_frontend
        from numba.core.cpu import InlineOptions

        # try and get a definition for the call, this isn't always possible as
        # it might be a eval(str)/part generated awaiting update etc. (parfors)
        to_inline = None
        try:
            to_inline = state.func_ir.get_definition(expr.func)
        except Exception:
            if self._DEBUG:
                print("Cannot find definition for %s" % expr.func)
            return False
        # do not handle closure inlining here, another pass deals with that.
        if getattr(to_inline, 'op', False) == 'make_function':
            return False

        # see if the definition is a "getattr", in which case walk the IR to
        # try and find the python function via the module from which it's
        # imported, this should all be encoded in the IR.
        if getattr(to_inline, 'op', False) == 'getattr':
            val = resolve_func_from_module(state.func_ir, to_inline)
        else:
            # This is likely a freevar or global
            #
            # NOTE: getattr 'value' on a call may fail if it's an ir.Expr as
            # getattr is overloaded to look in _kws.
            try:
                val = getattr(to_inline, 'value', False)
            except Exception:
                raise GuardException

        # if something was found...
        if val:
            # check it's dispatcher-like, the targetoptions attr holds the
            # kwargs supplied in the jit decorator and is where 'inline' will
            # be if it is present.
            topt = getattr(val, 'targetoptions', False)
            if topt:
                inline_type = topt.get('inline', None)
                # has 'inline' been specified?
                if inline_type is not None:
                    inline_opt = InlineOptions(inline_type)
                    # Could this be inlinable?
                    if not inline_opt.is_never_inline:
                        # yes, it could be inlinable
                        do_inline = True
                        pyfunc = val.py_func
                        # Has it got an associated cost model?
                        if inline_opt.has_cost_model:
                            # yes, it has a cost model, use it to determine
                            # whether to do the inline
                            py_func_ir = run_frontend(pyfunc)
                            do_inline = inline_type(expr, state.func_ir,
                                                    py_func_ir)
                        # if do_inline is True then inline!
                        if do_inline:
                            _, _, _, new_blocks = \
                                inline_worker.inline_function(state.func_ir,
                                                              block, i, pyfunc,)
                            if work_list is not None:
                                for blk in new_blocks:
                                    work_list.append(blk)
                            return True
        return False


@register_pass(mutates_CFG=False, analysis_only=False)
class PreserveIR(AnalysisPass):
    """
    Preserves the IR in the metadata
    """

    _name = "preserve_ir"

    def __init__(self):
        AnalysisPass.__init__(self)

    def run_pass(self, state):
        state.metadata['preserved_ir'] = state.func_ir.copy()
        return False


@register_pass(mutates_CFG=False, analysis_only=True)
class FindLiterallyCalls(FunctionPass):
    """Find calls to `numba.literally()` and signal if its requirement is not
    satisfied.
    """
    _name = "find_literally"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        find_literally_calls(state.func_ir, state.args)
        return False


@register_pass(mutates_CFG=True, analysis_only=False)
class CanonicalizeLoopExit(FunctionPass):
    """A pass to canonicalize loop exit by splitting it from function exit.
    """
    _name = "canonicalize_loop_exit"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        fir = state.func_ir
        cfg = compute_cfg_from_blocks(fir.blocks)
        status = False
        for loop in cfg.loops().values():
            for exit_label in loop.exits:
                if exit_label in cfg.exit_points():
                    self._split_exit_block(fir, cfg, exit_label)
                    status = True

        fir._reset_analysis_variables()

        vlt = postproc.VariableLifetime(fir.blocks)
        fir.variable_lifetime = vlt
        return status

    def _split_exit_block(self, fir, cfg, exit_label):
        curblock = fir.blocks[exit_label]
        newlabel = exit_label + 1
        newlabel = find_max_label(fir.blocks) + 1
        fir.blocks[newlabel] = curblock
        newblock = ir.Block(scope=curblock.scope, loc=curblock.loc)
        newblock.append(ir.Jump(newlabel, loc=curblock.loc))
        fir.blocks[exit_label] = newblock
        # Rename all labels
        fir.blocks = rename_labels(fir.blocks)


@register_pass(mutates_CFG=True, analysis_only=False)
class CanonicalizeLoopEntry(FunctionPass):
    """A pass to canonicalize loop header by splitting it from function entry.

    This is needed for loop-lifting; esp in py3.8
    """
    _name = "canonicalize_loop_entry"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        fir = state.func_ir
        cfg = compute_cfg_from_blocks(fir.blocks)
        status = False
        for loop in cfg.loops().values():
            if len(loop.entries) == 1:
                [entry_label] = loop.entries
                if entry_label == cfg.entry_point():
                    self._split_entry_block(fir, cfg, loop, entry_label)
                    status = True
        fir._reset_analysis_variables()

        vlt = postproc.VariableLifetime(fir.blocks)
        fir.variable_lifetime = vlt
        return status

    def _split_entry_block(self, fir, cfg, loop, entry_label):
        # Find iterator inputs into the for-loop header
        header_block = fir.blocks[loop.header]
        deps = set()
        for expr in header_block.find_exprs(op="iternext"):
            deps.add(expr.value)
        # Find the getiter for each iterator
        entry_block = fir.blocks[entry_label]

        # Find the start of loop entry statement that needs to be included.
        startpt = None
        list_of_insts = list(entry_block.find_insts(ir.Assign))
        for assign in reversed(list_of_insts):
            if assign.target in deps:
                rhs = assign.value
                if isinstance(rhs, ir.Var):
                    if rhs.is_temp:
                        deps.add(rhs)
                elif isinstance(rhs, ir.Expr):
                    expr = rhs
                    if expr.op == 'getiter':
                        startpt = assign
                        if expr.value.is_temp:
                            deps.add(expr.value)
                    elif expr.op == 'call':
                        defn = guard(get_definition, fir, expr.func)
                        if isinstance(defn, ir.Global):
                            if expr.func.is_temp:
                                deps.add(expr.func)
                elif isinstance(rhs, ir.Global) and rhs.value is range:
                    startpt = assign

        if startpt is None:
            return

        splitpt = entry_block.body.index(startpt)
        new_block = entry_block.copy()
        new_block.body = new_block.body[splitpt:]
        new_block.loc = new_block.body[0].loc
        new_label = find_max_label(fir.blocks) + 1
        entry_block.body = entry_block.body[:splitpt]
        entry_block.append(ir.Jump(new_label, loc=new_block.loc))

        fir.blocks[new_label] = new_block
        # Rename all labels
        fir.blocks = rename_labels(fir.blocks)


@register_pass(mutates_CFG=False, analysis_only=True)
class PrintIRCFG(FunctionPass):
    _name = "print_ir_cfg"

    def __init__(self):
        FunctionPass.__init__(self)
        self._ver = 0

    def run_pass(self, state):
        fir = state.func_ir
        self._ver += 1
        fir.render_dot(filename_prefix='v{}'.format(self._ver)).render()
        return False


@register_pass(mutates_CFG=True, analysis_only=False)
class MakeFunctionToJitFunction(FunctionPass):
    """
    This swaps an ir.Expr.op == "make_function" i.e. a closure, for a compiled
    function containing the closure body and puts it in ir.Global. It's a 1:1
    statement value swap. `make_function` is already untyped
    """
    _name = "make_function_op_code_to_jit_function"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        from numba import njit
        func_ir = state.func_ir
        mutated = False
        for idx, blk in func_ir.blocks.items():
            for stmt in blk.body:
                if isinstance(stmt, ir.Assign):
                    if isinstance(stmt.value, ir.Expr):
                        if stmt.value.op == "make_function":
                            node = stmt.value
                            getdef = func_ir.get_definition
                            kw_default = getdef(node.defaults)
                            ok = False
                            if (kw_default is None or
                                    isinstance(kw_default, ir.Const)):
                                ok = True
                            elif isinstance(kw_default, tuple):
                                ok = all([isinstance(getdef(x), ir.Const)
                                          for x in kw_default])
                            elif isinstance(kw_default, ir.Expr):
                                if kw_default.op != "build_tuple":
                                    continue
                                ok = all([isinstance(getdef(x), ir.Const)
                                          for x in kw_default.items])
                            if not ok:
                                continue

                            pyfunc = convert_code_obj_to_function(node, func_ir)
                            func = njit()(pyfunc)
                            new_node = ir.Global(node.code.co_name, func,
                                                 stmt.loc)
                            stmt.value = new_node
                            mutated |= True

        # if a change was made the del ordering is probably wrong, patch up
        if mutated:
            post_proc = postproc.PostProcessor(func_ir)
            post_proc.run()

        return mutated


@register_pass(mutates_CFG=True, analysis_only=False)
class TransformLiteralUnrollConstListToTuple(FunctionPass):
    """ This pass spots a `literal_unroll([<constant values>])` and rewrites it
    as a `literal_unroll(tuple(<constant values>))`.
    """
    _name = "transform_literal_unroll_const_list_to_tuple"

    _accepted_types = (types.BaseTuple, types.LiteralList)

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        mutated = False
        func_ir = state.func_ir
        for label, blk in func_ir.blocks.items():
            calls = [_ for _ in blk.find_exprs('call')]
            for call in calls:
                glbl = guard(get_definition, func_ir, call.func)
                if glbl and isinstance(glbl, (ir.Global, ir.FreeVar)):
                    # find a literal_unroll
                    if glbl.value is literal_unroll:
                        if len(call.args) > 1:
                            msg = "literal_unroll takes one argument, found %s"
                            raise errors.UnsupportedError(msg % len(call.args),
                                                          call.loc)
                        # get the arg, make sure its a build_list
                        unroll_var = call.args[0]
                        to_unroll = guard(get_definition, func_ir, unroll_var)
                        if (isinstance(to_unroll, ir.Expr) and
                                to_unroll.op == "build_list"):
                            # make sure they are all const items in the list
                            for i, item in enumerate(to_unroll.items):
                                val = guard(get_definition, func_ir, item)
                                if not val:
                                    msg = ("multiple definitions for variable "
                                           "%s, cannot resolve constant")
                                    raise errors.UnsupportedError(msg % item,
                                                                  to_unroll.loc)
                                if not isinstance(val, ir.Const):
                                    msg = ("Found non-constant value at "
                                           "position %s in a list argument to "
                                           "literal_unroll" % i)
                                    raise errors.UnsupportedError(msg,
                                                                  to_unroll.loc)
                            # The above appears ok, now swap the build_list for
                            # a built tuple.

                            # find the assignment for the unroll target
                            to_unroll_lhs = guard(get_definition, func_ir,
                                                  unroll_var, lhs_only=True)

                            if to_unroll_lhs is None:
                                msg = ("multiple definitions for variable "
                                       "%s, cannot resolve constant")
                                raise errors.UnsupportedError(msg % unroll_var,
                                                              to_unroll.loc)
                            # scan all blocks looking for the LHS
                            for b in func_ir.blocks.values():
                                asgn = b.find_variable_assignment(
                                    to_unroll_lhs.name)
                                if asgn is not None:
                                    break
                            else:
                                msg = ("Cannot find assignment for known "
                                       "variable %s") % to_unroll_lhs.name
                                raise errors.CompilerError(msg, to_unroll.loc)

                            # Create a tuple with the list items as contents
                            tup = ir.Expr.build_tuple(to_unroll.items,
                                                      to_unroll.loc)

                            # swap the list for the tuple
                            asgn.value = tup
                            mutated = True
                        elif (isinstance(to_unroll, ir.Expr) and
                              to_unroll.op == "build_tuple"):
                            # this is fine, do nothing
                            pass
                        elif (isinstance(to_unroll, (ir.Global, ir.FreeVar)) and
                              isinstance(to_unroll.value, tuple)):
                            # this is fine, do nothing
                            pass
                        elif isinstance(to_unroll, ir.Arg):
                            # this is only fine if the arg is a tuple
                            ty = state.typemap[to_unroll.name]
                            if not isinstance(ty, self._accepted_types):
                                msg = ("Invalid use of literal_unroll with a "
                                       "function argument, only tuples are "
                                       "supported as function arguments, found "
                                       "%s") % ty
                                raise errors.UnsupportedError(msg,
                                                              to_unroll.loc)
                        else:
                            extra = None
                            if isinstance(to_unroll, ir.Expr):
                                # probably a slice
                                if to_unroll.op == "getitem":
                                    ty = state.typemap[to_unroll.value.name]
                                    # check if this is a tuple slice
                                    if not isinstance(ty, self._accepted_types):
                                        extra = "operation %s" % to_unroll.op
                                        loc = to_unroll.loc
                            elif isinstance(to_unroll, ir.Arg):
                                extra = "non-const argument %s" % to_unroll.name
                                loc = to_unroll.loc
                            else:
                                if to_unroll is None:
                                    extra = ('multiple definitions of '
                                             'variable "%s".' % unroll_var.name)
                                    loc = unroll_var.loc
                                else:
                                    loc = to_unroll.loc
                                    extra = "unknown problem"

                            if extra:
                                msg = ("Invalid use of literal_unroll, "
                                       "argument should be a tuple or a list "
                                       "of constant values. Failure reason: "
                                       "found %s" % extra)
                                raise errors.UnsupportedError(msg, loc)
        return mutated


@register_pass(mutates_CFG=True, analysis_only=False)
class MixedContainerUnroller(FunctionPass):
    _name = "mixed_container_unroller"

    _DEBUG = False

    _accepted_types = (types.BaseTuple, types.LiteralList)

    def __init__(self):
        FunctionPass.__init__(self)

    def analyse_tuple(self, tup):
        """
        Returns a map of type->list(indexes) for a typed tuple
        """
        d = defaultdict(list)
        for i, ty in enumerate(tup):
            d[ty].append(i)
        return d

    def add_offset_to_labels_w_ignore(self, blocks, offset, ignore=None):
        """add an offset to all block labels and jump/branch targets
        don't add an offset to anything in the ignore list
        """
        if ignore is None:
            ignore = set()

        new_blocks = {}
        for l, b in blocks.items():
            # some parfor last blocks might be empty
            term = None
            if b.body:
                term = b.body[-1]
            if isinstance(term, ir.Jump):
                if term.target not in ignore:
                    b.body[-1] = ir.Jump(term.target + offset, term.loc)
            if isinstance(term, ir.Branch):
                if term.truebr not in ignore:
                    new_true = term.truebr + offset
                else:
                    new_true = term.truebr

                if term.falsebr not in ignore:
                    new_false = term.falsebr + offset
                else:
                    new_false = term.falsebr
                b.body[-1] = ir.Branch(term.cond, new_true, new_false, term.loc)
            new_blocks[l + offset] = b
        return new_blocks

    def inject_loop_body(self, switch_ir, loop_ir, caller_max_label,
                         dont_replace, switch_data):
        """
        Injects the "loop body" held in `loop_ir` into `switch_ir` where ever
        there is a statement of the form `SENTINEL.<int> = RHS`. It also:
        * Finds and then deliberately does not relabel non-local jumps so as to
          make the switch table suitable for injection into the IR from which
          the loop body was derived.
        * Looks for `typed_getitem` and wires them up to loop body version
          specific variables or, if possible, directly writes in their constant
          value at their use site.

        Args:
        - switch_ir, the switch table with SENTINELS as generated by
          self.gen_switch
        - loop_ir, the IR of the loop blocks (derived from the original func_ir)
        - caller_max_label, the maximum label in the func_ir caller
        - dont_replace, variables that should not be renamed (to handle
          references to variables that are incoming at the loop head/escaping at
          the loop exit.
        - switch_data, the switch table data used to generated the switch_ir,
          can be generated by self.analyse_tuple.

        Returns:
        - A type specific switch table with each case containing a versioned
          loop body suitable for injection as a replacement for the loop_ir.
        """

        # Switch IR came from code gen, immediately relabel to prevent
        # collisions with IR derived from the user code (caller)
        switch_ir.blocks = self.add_offset_to_labels_w_ignore(
            switch_ir.blocks, caller_max_label + 1)

        # Find the sentinels and validate the form
        sentinel_exits = set()
        sentinel_blocks = []
        for lbl, blk in switch_ir.blocks.items():
            for i, stmt in enumerate(blk.body):
                if isinstance(stmt, ir.Assign):
                    if "SENTINEL" in stmt.target.name:
                        sentinel_blocks.append(lbl)
                        sentinel_exits.add(blk.body[-1].target)
                        break

        assert len(sentinel_exits) == 1  # should only be 1 exit
        switch_ir.blocks.pop(sentinel_exits.pop())  # kill the exit, it's dead

        # find jumps that are non-local, we won't relabel these
        ignore_set = set()
        local_lbl = [x for x in loop_ir.blocks.keys()]
        for lbl, blk in loop_ir.blocks.items():
            for i, stmt in enumerate(blk.body):
                if isinstance(stmt, ir.Jump):
                    if stmt.target not in local_lbl:
                        ignore_set.add(stmt.target)
                if isinstance(stmt, ir.Branch):
                    if stmt.truebr not in local_lbl:
                        ignore_set.add(stmt.truebr)
                    if stmt.falsebr not in local_lbl:
                        ignore_set.add(stmt.falsebr)

        # make sure the generated switch table matches the switch data
        assert len(sentinel_blocks) == len(switch_data)

        # replace the sentinel_blocks with the loop body
        for lbl, branch_ty in zip(sentinel_blocks, switch_data.keys()):
            loop_blocks = deepcopy(loop_ir.blocks)
            # relabel blocks WRT switch table, each block replacement will shift
            # the maximum label
            max_label = max(switch_ir.blocks.keys())
            loop_blocks = self.add_offset_to_labels_w_ignore(
                loop_blocks, max_label + 1, ignore_set)

            # start label
            loop_start_lbl = min(loop_blocks.keys())

            # fix the typed_getitem locations in the loop blocks
            for blk in loop_blocks.values():
                new_body = []
                for stmt in blk.body:
                    if isinstance(stmt, ir.Assign):
                        if (isinstance(stmt.value, ir.Expr) and
                                stmt.value.op == "typed_getitem"):
                            if isinstance(branch_ty, types.Literal):
                                new_const_name = mk_unique_var("branch_const")
                                new_const_var = ir.Var(
                                    blk.scope, new_const_name, stmt.loc)
                                new_const_val = ir.Const(
                                    branch_ty.literal_value, stmt.loc)
                                const_assign = ir.Assign(
                                    new_const_val, new_const_var, stmt.loc)
                                new_assign = ir.Assign(
                                    new_const_var, stmt.target, stmt.loc)
                                new_body.append(const_assign)
                                new_body.append(new_assign)
                                dont_replace.append(new_const_name)
                            else:
                                orig = stmt.value
                                new_typed_getitem = ir.Expr.typed_getitem(
                                    value=orig.value, dtype=branch_ty,
                                    index=orig.index, loc=orig.loc)
                                new_assign = ir.Assign(
                                    new_typed_getitem, stmt.target, stmt.loc)
                                new_body.append(new_assign)
                        else:
                            new_body.append(stmt)
                    else:
                        new_body.append(stmt)
                blk.body = new_body

            # rename
            var_table = get_name_var_table(loop_blocks)
            drop_keys = []
            for k, v in var_table.items():
                if v.name in dont_replace:
                    drop_keys.append(k)
            for k in drop_keys:
                var_table.pop(k)

            new_var_dict = {}
            for name, var in var_table.items():
                new_var_dict[name] = mk_unique_var(name)
            replace_var_names(loop_blocks, new_var_dict)

            # clobber the sentinel body and then stuff in the rest
            switch_ir.blocks[lbl] = deepcopy(loop_blocks[loop_start_lbl])
            remaining_keys = [y for y in loop_blocks.keys()]
            remaining_keys.remove(loop_start_lbl)
            for k in remaining_keys:
                switch_ir.blocks[k] = deepcopy(loop_blocks[k])

        if self._DEBUG:
            print("-" * 80 + "EXIT STUFFER")
            switch_ir.dump()
            print("-" * 80)

        return switch_ir

    def gen_switch(self, data, index):
        """
        Generates a function with a switch table like
        def foo():
            if PLACEHOLDER_INDEX in (<integers>):
                SENTINEL = None
            elif PLACEHOLDER_INDEX in (<integers>):
                SENTINEL = None
            ...
            else:
                raise RuntimeError

        The data is a map of (type : indexes) for example:
        (int64, int64, float64)
        might give:
        {int64: [0, 1], float64: [2]}

        The index is the index variable for the driving range loop over the
        mixed tuple.
        """
        elif_tplt = "\n\telif PLACEHOLDER_INDEX in (%s,):\n\t\tSENTINEL = None"

        b = ('def foo():\n\tif PLACEHOLDER_INDEX in (%s,):\n\t\t'
             'SENTINEL = None\n%s\n\telse:\n\t\t'
             'raise RuntimeError("Unreachable")')
        keys = [k for k in data.keys()]

        elifs = []
        for i in range(1, len(keys)):
            elifs.append(elif_tplt % ','.join(map(str, data[keys[i]])))
        src = b % (','.join(map(str, data[keys[0]])), ''.join(elifs))
        wstr = src
        l = {}
        exec(wstr, {}, l)
        bfunc = l['foo']
        branches = compile_to_numba_ir(bfunc, {})
        for lbl, blk in branches.blocks.items():
            for stmt in blk.body:
                if isinstance(stmt, ir.Assign):
                    if isinstance(stmt.value, ir.Global):
                        if stmt.value.name == "PLACEHOLDER_INDEX":
                            stmt.value = index
        return branches

    def apply_transform(self, state):
        # compute new CFG
        func_ir = state.func_ir
        cfg = compute_cfg_from_blocks(func_ir.blocks)
        # find loops
        loops = cfg.loops()

        # 0. Find the loops containing literal_unroll and store this
        #    information
        literal_unroll_info = dict()
        unroll_info = namedtuple(
            "unroll_info", [
                "loop", "call", "arg", "getitem"])

        def get_call_args(init_arg, want):
            # Chases the assignment of a called value back through a specific
            # call to a global function "want" and returns the arguments
            # supplied to that function's call
            some_call = get_definition(func_ir, init_arg)
            if not isinstance(some_call, ir.Expr):
                raise GuardException
            if not some_call.op == "call":
                raise GuardException
            the_global = get_definition(func_ir, some_call.func)
            if not isinstance(the_global, ir.Global):
                raise GuardException
            if the_global.value is not want:
                raise GuardException
            return some_call

        for lbl, loop in loops.items():
            # TODO: check the loop head has literal_unroll, if it does but
            # does not conform to the following then raise

            # scan loop header
            iternexts = [_ for _ in
                         func_ir.blocks[loop.header].find_exprs('iternext')]
            if len(iternexts) != 1:
                return False
            for iternext in iternexts:
                # Walk the canonicalised loop structure and check it
                # Check loop form range(literal_unroll(container)))
                phi = guard(get_definition, func_ir,  iternext.value)
                if phi is None:
                    continue

                # check call global "range"
                range_call = guard(get_call_args, phi.value, range)
                if range_call is None:
                    continue
                range_arg = range_call.args[0]

                # check call global "len"
                len_call = guard(get_call_args, range_arg, len)
                if len_call is None:
                    continue
                len_arg = len_call.args[0]

                # check literal_unroll
                literal_unroll_call = guard(get_definition, func_ir, len_arg)
                if literal_unroll_call is None:
                    continue
                if not isinstance(literal_unroll_call, ir.Expr):
                    continue
                if literal_unroll_call.op != "call":
                    continue
                literal_func = getattr(literal_unroll_call, 'func', None)
                if not literal_func:
                    continue
                call_func = guard(get_definition, func_ir,
                                  literal_unroll_call.func)
                if call_func is None:
                    continue
                call_func = call_func.value

                if call_func is literal_unroll:
                    assert len(literal_unroll_call.args) == 1
                    arg = literal_unroll_call.args[0]
                    typemap = state.typemap
                    resolved_arg = guard(get_definition, func_ir, arg,
                                         lhs_only=True)
                    ty = typemap[resolved_arg.name]
                    assert isinstance(ty, self._accepted_types)
                    # loop header is spelled ok, now make sure the body
                    # actually contains a getitem

                    # find a "getitem"
                    tuple_getitem = None
                    for lbl in loop.body:
                        blk = func_ir.blocks[lbl]
                        for stmt in blk.body:
                            if isinstance(stmt, ir.Assign):
                                if (isinstance(stmt.value, ir.Expr) and
                                        stmt.value.op == "getitem"):
                                    # check for something like a[i]
                                    if stmt.value.value != arg:
                                        # that failed, so check for the
                                        # definition
                                        dfn = guard(get_definition, func_ir,
                                                    stmt.value.value)
                                        if dfn is None:
                                            continue
                                        try:
                                            args = getattr(dfn, 'args', False)
                                        except KeyError:
                                            continue
                                        if not args:
                                            continue
                                        if not args[0] == arg:
                                            continue
                                    target_ty = state.typemap[arg.name]
                                    if not isinstance(target_ty,
                                                      self._accepted_types):
                                        continue
                                    tuple_getitem = stmt
                                    break
                        if tuple_getitem:
                            break
                    else:
                        continue  # no getitem in this loop

                    ui = unroll_info(loop, literal_unroll_call, arg,
                                     tuple_getitem)
                    literal_unroll_info[lbl] = ui

        if not literal_unroll_info:
            return False

        # 1. Validate loops, must not have any calls to literal_unroll
        for test_lbl, test_loop in literal_unroll_info.items():
            for ref_lbl, ref_loop in literal_unroll_info.items():
                if test_lbl == ref_lbl:  # comparing to self! skip
                    continue
                if test_loop.loop.header in ref_loop.loop.body:
                    msg = ("Nesting of literal_unroll is unsupported")
                    loc = func_ir.blocks[test_loop.loop.header].loc
                    raise errors.UnsupportedError(msg, loc)

        # 2. Do the unroll, get a loop and process it!
        lbl, info = literal_unroll_info.popitem()
        self.unroll_loop(state, info)

        # 3. Rebuild the state, the IR has taken a hammering
        func_ir.blocks = simplify_CFG(func_ir.blocks)
        post_proc = postproc.PostProcessor(func_ir)
        post_proc.run()
        if self._DEBUG:
            print('-' * 80 + "END OF PASS, SIMPLIFY DONE")
            func_ir.dump()
        func_ir._definitions = build_definitions(func_ir.blocks)
        return True

    def unroll_loop(self, state, loop_info):
        # The general idea here is to:
        # 1. Find *a* getitem that conforms to the literal_unroll semantic,
        #    i.e. one that is targeting a tuple with a loop induced index
        # 2. Compute a structure from the tuple that describes which
        #    iterations of a loop will have which type
        # 3. Generate a switch table in IR form for the structure in 2
        # 4. Switch out getitems for the tuple for a `typed_getitem`
        # 5. Inject switch table as replacement loop body
        # 6. Patch up
        func_ir = state.func_ir
        getitem_target = loop_info.arg
        target_ty = state.typemap[getitem_target.name]
        assert isinstance(target_ty, self._accepted_types)

        # 1. find a "getitem" that conforms
        tuple_getitem = []
        for lbl in loop_info.loop.body:
            blk = func_ir.blocks[lbl]
            for stmt in blk.body:
                if isinstance(stmt, ir.Assign):
                    if isinstance(stmt.value,
                                  ir.Expr) and stmt.value.op == "getitem":
                        # try a couple of spellings... a[i] and ref(a)[i]
                        if stmt.value.value != getitem_target:
                            dfn = func_ir.get_definition(stmt.value.value)
                            try:
                                args = getattr(dfn, 'args', False)
                            except KeyError:
                                continue
                            if not args:
                                continue
                            if not args[0] == getitem_target:
                                continue
                        target_ty = state.typemap[getitem_target.name]
                        if not isinstance(target_ty, self._accepted_types):
                            continue
                        tuple_getitem.append(stmt)

        if not tuple_getitem:
            msg = ("Loop unrolling analysis has failed, there's no getitem "
                   "in loop body that conforms to literal_unroll "
                   "requirements.")
            LOC = func_ir.blocks[loop_info.loop.header].loc
            raise errors.CompilerError(msg, LOC)

        # 2. get switch data
        switch_data = self.analyse_tuple(target_ty)

        # 3. generate switch IR
        index = func_ir._definitions[tuple_getitem[0].value.index.name][0]
        branches = self.gen_switch(switch_data, index)

        # 4. swap getitems for a typed_getitem, these are actually just
        # placeholders at this point. When the loop is duplicated they can
        # be swapped for a typed_getitem of the correct type or if the item
        # is literal it can be shoved straight into the duplicated loop body
        for item in tuple_getitem:
            old = item.value
            new = ir.Expr.typed_getitem(
                old.value, types.void, old.index, old.loc)
            item.value = new

        # 5. Inject switch table

        # Find the actual loop without the header (that won't get replaced)
        # and derive some new IR for this set of blocks
        this_loop = loop_info.loop
        this_loop_body = this_loop.body - \
            set([this_loop.header])
        loop_blocks = {
            x: func_ir.blocks[x] for x in this_loop_body}
        new_ir = func_ir.derive(loop_blocks)

        # Work out what is live on entry and exit so as to prevent
        # replacement (defined vars can escape, used vars live at the header
        # need to remain as-is so their references are correct, they can
        # also escape).

        usedefs = compute_use_defs(func_ir.blocks)
        idx = this_loop.header
        keep = set()
        keep |= usedefs.usemap[idx] | usedefs.defmap[idx]
        keep |= func_ir.variable_lifetime.livemap[idx]
        dont_replace = [x for x in (keep)]

        # compute the unrolled body
        unrolled_body = self.inject_loop_body(
            branches, new_ir, max(func_ir.blocks.keys()) + 1,
            dont_replace, switch_data)

        # 6. Patch in the unrolled body and fix up
        blks = state.func_ir.blocks
        the_scope = next(iter(blks.values())).scope
        orig_lbl = tuple(this_loop_body)

        replace, *delete = orig_lbl
        unroll, header_block = unrolled_body, this_loop.header
        unroll_lbl = [x for x in sorted(unroll.blocks.keys())]
        blks[replace] = transfer_scope(unroll.blocks[unroll_lbl[0]], the_scope)
        [blks.pop(d) for d in delete]
        for k in unroll_lbl[1:]:
            blks[k] = transfer_scope(unroll.blocks[k], the_scope)
        # stitch up the loop predicate true -> new loop body jump
        blks[header_block].body[-1].truebr = replace

    def run_pass(self, state):
        mutated = False
        func_ir = state.func_ir
        # first limit the work by squashing the CFG if possible
        func_ir.blocks = simplify_CFG(func_ir.blocks)

        if self._DEBUG:
            print("-" * 80 + "PASS ENTRY")
            func_ir.dump()
            print("-" * 80)

        # limitations:
        # 1. No nested unrolls
        # 2. Opt in via `numba.literal_unroll`
        # 3. No multiple mix-tuple use

        # keep running the transform loop until it reports no more changes
        while(True):
            stat = self.apply_transform(state)
            mutated |= stat
            if not stat:
                break

        # reset type inference now we are done with the partial results
        state.typemap = {}
        state.calltypes = None

        return mutated


@register_pass(mutates_CFG=True, analysis_only=False)
class IterLoopCanonicalization(FunctionPass):
    """ Transforms loops that are induced by `getiter` into range() driven loops
    If the typemap is available this will only impact Tuple and UniTuple, if it
    is not available it will impact all matching loops.
    """
    _name = "iter_loop_canonicalisation"

    _DEBUG = False

    # if partial typing info is available it will only look at these types
    _accepted_types = (types.BaseTuple, types.LiteralList)
    _accepted_calls = (literal_unroll,)

    def __init__(self):
        FunctionPass.__init__(self)

    def assess_loop(self, loop, func_ir, partial_typemap=None):
        # it's a iter loop if:
        # - loop header is driven by an iternext
        # - the iternext value is a phi derived from getiter()

        # check header
        iternexts = [_ for _ in
                     func_ir.blocks[loop.header].find_exprs('iternext')]
        if len(iternexts) != 1:
            return False
        for iternext in iternexts:
            phi = guard(get_definition, func_ir,  iternext.value)
            if phi is None:
                return False
            if getattr(phi, 'op', False) == 'getiter':
                if partial_typemap:
                    # check that the call site is accepted, until we're
                    # confident that tuple unrolling is behaving require opt-in
                    # guard of `literal_unroll`, remove this later!
                    phi_val_defn = guard(get_definition, func_ir,  phi.value)
                    if not isinstance(phi_val_defn, ir.Expr):
                        return False
                    if not phi_val_defn.op == "call":
                        return False
                    call = guard(get_definition, func_ir,  phi_val_defn)
                    if call is None or len(call.args) != 1:
                        return False
                    func_var = guard(get_definition, func_ir,  call.func)
                    func = guard(get_definition, func_ir,  func_var)
                    if func is None or not isinstance(func,
                                                      (ir.Global, ir.FreeVar)):
                        return False
                    if (func.value is None or
                            func.value not in self._accepted_calls):
                        return False

                    # now check the type is supported
                    ty = partial_typemap.get(call.args[0].name, None)
                    if ty and isinstance(ty, self._accepted_types):
                        return len(loop.entries) == 1
                else:
                    return len(loop.entries) == 1

    def transform(self, loop, func_ir, cfg):
        def get_range(a):
            return range(len(a))

        def tokenise(x):
            return mk_unique_var("CANONICALISER_%s" % x)

        iternext = [_ for _ in
                    func_ir.blocks[loop.header].find_exprs('iternext')][0]
        LOC = func_ir.blocks[loop.header].loc
        get_range_var = ir.Var(func_ir.blocks[loop.header].scope,
                               tokenise('get_range_gbl'), LOC)
        get_range_global = ir.Global('get_range', get_range, LOC)
        assgn = ir.Assign(get_range_global, get_range_var, LOC)

        loop_entry = tuple(loop.entries)[0]
        entry_block = func_ir.blocks[loop_entry]
        entry_block.body.insert(0, assgn)

        iterarg = guard(get_definition, func_ir,  iternext.value)
        if iterarg is not None:
            iterarg = iterarg.value

        # look for iternext
        idx = 0
        for stmt in entry_block.body:
            if isinstance(stmt, ir.Assign):
                if isinstance(stmt.value,
                              ir.Expr) and stmt.value.op == 'getiter':
                    break
            idx += 1
        else:
            raise ValueError("problem")

        # create a range(len(tup)) and inject it
        call_get_range_var = ir.Var(entry_block.scope,
                                    tokenise('call_get_range'), LOC)
        make_call = ir.Expr.call(get_range_var, (stmt.value.value,), (), LOC)
        assgn_call = ir.Assign(make_call, call_get_range_var, LOC)
        entry_block.body.insert(idx, assgn_call)
        entry_block.body[idx + 1].value.value = call_get_range_var

        glbls = copy(func_ir.func_id.func.__globals__)
        from numba.core.inline_closurecall import inline_closure_call
        inline_closure_call(func_ir, glbls, entry_block, idx, get_range,)
        kill = entry_block.body.index(assgn)
        entry_block.body.pop(kill)

        # find the induction variable + references in the loop header
        # fixed point iter to do this, it's a bit clunky
        induction_vars = set()
        header_block = func_ir.blocks[loop.header]

        # find induction var
        ind = [x for x in header_block.find_exprs('pair_first')]
        for x in ind:
            induction_vars.add(func_ir.get_assignee(x, loop.header))
        # find aliases of the induction var
        tmp = set()
        for x in induction_vars:
            try:  # there's not always an alias, e.g. loop from inlined closure
                tmp.add(func_ir.get_assignee(x, loop.header))
            except ValueError:
                pass
        induction_vars |= tmp
        induction_var_names = set([x.name for x in induction_vars])

        # Find the downstream blocks that might reference the induction var
        succ = set()
        for lbl in loop.exits:
            succ |= set([x[0] for x in cfg.successors(lbl)])
        check_blocks = (loop.body | loop.exits | succ) ^ {loop.header}

        # replace RHS use of induction var with getitem
        for lbl in check_blocks:
            for stmt in func_ir.blocks[lbl].body:
                if isinstance(stmt, ir.Assign):
                    # check for aliases
                    try:
                        lookup = getattr(stmt.value, 'name', None)
                    except KeyError:
                        continue
                    if lookup and lookup in induction_var_names:
                        stmt.value = ir.Expr.getitem(
                            iterarg, stmt.value, stmt.loc)

        post_proc = postproc.PostProcessor(func_ir)
        post_proc.run()

    def run_pass(self, state):
        func_ir = state.func_ir
        cfg = compute_cfg_from_blocks(func_ir.blocks)
        loops = cfg.loops()

        mutated = False
        for header, loop in loops.items():
            stat = self.assess_loop(loop, func_ir, state.typemap)
            if stat:
                if self._DEBUG:
                    print("Canonicalising loop", loop)
                self.transform(loop, func_ir, cfg)
                mutated = True
            else:
                if self._DEBUG:
                    print("NOT Canonicalising loop", loop)

        func_ir.blocks = simplify_CFG(func_ir.blocks)
        return mutated


@register_pass(mutates_CFG=True, analysis_only=False)
class LiteralUnroll(FunctionPass):
    """Implement the literal_unroll semantics"""
    _name = "literal_unroll"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        # Determine whether to even attempt this pass... if there's no
        # `literal_unroll as a global or as a freevar then just skip.
        found = False
        func_ir = state.func_ir
        for blk in func_ir.blocks.values():
            for asgn in blk.find_insts(ir.Assign):
                if isinstance(asgn.value, (ir.Global, ir.FreeVar)):
                    if asgn.value.value is literal_unroll:
                        found = True
                        break
            if found:
                break
        if not found:
            return False

        # run as subpipeline
        from numba.core.compiler_machinery import PassManager
        from numba.core.typed_passes import PartialTypeInference
        pm = PassManager("literal_unroll_subpipeline")
        # get types where possible to help with list->tuple change
        pm.add_pass(PartialTypeInference, "performs partial type inference")
        # make const lists tuples
        pm.add_pass(TransformLiteralUnrollConstListToTuple,
                    "switch const list for tuples")
        # recompute partial typemap following IR change
        pm.add_pass(PartialTypeInference, "performs partial type inference")
        # canonicalise loops
        pm.add_pass(IterLoopCanonicalization,
                    "switch iter loops for range driven loops")
        # rewrite consts
        pm.add_pass(RewriteSemanticConstants, "rewrite semantic constants")
        # do the unroll
        pm.add_pass(MixedContainerUnroller, "performs mixed container unroll")
        # rewrite dynamic getitem to static getitem as it's possible some more
        # getitems will now be statically resolvable
        pm.add_pass(GenericRewrites, "Generic Rewrites")
        pm.finalize()
        pm.run(state)
        return True


@register_pass(mutates_CFG=True, analysis_only=False)
class SimplifyCFG(FunctionPass):
    """Perform CFG simplification"""
    _name = "simplify_cfg"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        blks = state.func_ir.blocks
        new_blks = simplify_CFG(blks)
        state.func_ir.blocks = new_blks
        mutated = blks != new_blks
        return mutated


@register_pass(mutates_CFG=False, analysis_only=False)
class ReconstructSSA(FunctionPass):
    """Perform SSA-reconstruction

    Produces minimal SSA.
    """
    _name = "reconstruct_ssa"

    def __init__(self):
        FunctionPass.__init__(self)

    def run_pass(self, state):
        state.func_ir = reconstruct_ssa(state.func_ir)
        self._patch_locals(state)

        # Rebuild definitions
        state.func_ir._definitions = build_definitions(state.func_ir.blocks)

        # Rerun postprocessor to update metadata
        # example generator_info
        post_proc = postproc.PostProcessor(state.func_ir)
        post_proc.run(emit_dels=False)

        if config.DEBUG or config.DUMP_SSA:
            name = state.func_ir.func_id.func_qualname
            print(f"SSA IR DUMP: {name}".center(80, "-"))
            state.func_ir.dump()

        return True      # XXX detect if it actually got changed

    def _patch_locals(self, state):
        # Fix dispatcher locals dictionary type annotation
        locals_dict = state.get('locals')
        if locals_dict is None:
            return

        first_blk, *_ = state.func_ir.blocks.values()
        scope = first_blk.scope
        for parent, redefs in scope.var_redefinitions.items():
            if parent in locals_dict:
                typ = locals_dict[parent]
                for derived in redefs:
                    locals_dict[derived] = typ
