# Buggy code
def _stencil_wrapper(self, result, sigret, return_type, typemap, calltypes, *args):
    # Overall approach:
    # 1) Construct a string containing a function definition for the stencil function
    #    that will execute the stencil kernel.  This function definition includes a
    #    unique stencil function name, the parameters to the stencil kernel, loop
    #    nests across the dimenions of the input array.  Those loop nests use the
    #    computed stencil kernel size so as not to try to compute elements where
    #    elements outside the bounds of the input array would be needed.
    # 2) The but of the loop nest in this new function is a special sentinel
    #    assignment.
    # 3) Get the IR of this new function.
    # 4) Split the block containing the sentinel assignment and remove the sentinel
    #    assignment.  Insert the stencil kernel IR into the stencil function IR
    #    after label and variable renaming of the stencil kernel IR to prevent
    #    conflicts with the stencil function IR.
    # 5) Compile the combined stencil function IR + stencil kernel IR into existence.

    # Copy the kernel so that our changes for this callsite
    # won't effect other callsites.
    (kernel_copy, copy_calltypes) = self.copy_ir_with_calltypes(
        self.kernel_ir, calltypes
    )
    # The stencil kernel body becomes the body of a loop, for which args aren't needed.
    ir_utils.remove_args(kernel_copy.blocks)
    first_arg = kernel_copy.arg_names[0]

    in_cps, out_cps = ir_utils.copy_propagate(kernel_copy.blocks, typemap)
    name_var_table = ir_utils.get_name_var_table(kernel_copy.blocks)
    ir_utils.apply_copy_propagate(
        kernel_copy.blocks, in_cps, name_var_table, typemap, copy_calltypes
    )

    if "out" in name_var_table:
        raise ValueError("Cannot use the reserved word 'out' in stencil kernels.")

    sentinel_name = ir_utils.get_unused_var_name("__sentinel__", name_var_table)
    if config.DEBUG_ARRAY_OPT == 1:
        print("name_var_table", name_var_table, sentinel_name)

    the_array = args[0]

    if config.DEBUG_ARRAY_OPT == 1:
        print(
            "_stencil_wrapper",
            return_type,
            return_type.dtype,
            type(return_type.dtype),
            args,
        )
        ir_utils.dump_blocks(kernel_copy.blocks)

    # We generate a Numba function to execute this stencil and here
    # create the unique name of this function.
    stencil_func_name = "__numba_stencil_%s_%s" % (
        hex(id(the_array)).replace("-", "_"),
        self.id,
    )

    # We will put a loop nest in the generated function for each
    # dimension in the input array.  Here we create the name for
    # the index variable for each dimension.  index0, index1, ...
    index_vars = []
    for i in range(the_array.ndim):
        index_var_name = ir_utils.get_unused_var_name("index" + str(i), name_var_table)
        index_vars += [index_var_name]

    # Create extra signature for out and neighborhood.
    out_name = ir_utils.get_unused_var_name("out", name_var_table)
    neighborhood_name = ir_utils.get_unused_var_name("neighborhood", name_var_table)
    sig_extra = ""
    if result is not None:
        sig_extra += ", {}=None".format(out_name)
    if "neighborhood" in dict(self.kws):
        sig_extra += ", {}=None".format(neighborhood_name)

    # Get a list of the standard indexed array names.
    standard_indexed = self.options.get("standard_indexing", [])

    if first_arg in standard_indexed:
        raise ValueError(
            "The first argument to a stencil kernel must "
            "use relative indexing, not standard indexing."
        )

    if len(set(standard_indexed) - set(kernel_copy.arg_names)) != 0:
        raise ValueError(
            "Standard indexing requested for an array name "
            "not present in the stencil kernel definition."
        )

    # Add index variables to getitems in the IR to transition the accesses
    # in the kernel from relative to regular Python indexing.  Returns the
    # computed size of the stencil kernel and a list of the relatively indexed
    # arrays.
    kernel_size, relatively_indexed = self.add_indices_to_kernel(
        kernel_copy, index_vars, the_array.ndim, self.neighborhood, standard_indexed
    )
    if self.neighborhood is None:
        self.neighborhood = kernel_size

    if config.DEBUG_ARRAY_OPT == 1:
        print("After add_indices_to_kernel")
        ir_utils.dump_blocks(kernel_copy.blocks)

    # The return in the stencil kernel becomes a setitem for that
    # particular point in the iteration space.
    ret_blocks = self.replace_return_with_setitem(
        kernel_copy.blocks, index_vars, out_name
    )

    if config.DEBUG_ARRAY_OPT == 1:
        print("After replace_return_with_setitem", ret_blocks)
        ir_utils.dump_blocks(kernel_copy.blocks)

    # Start to form the new function to execute the stencil kernel.
    func_text = "def {}({}{}):\n".format(
        stencil_func_name, ",".join(kernel_copy.arg_names), sig_extra
    )

    # Get loop ranges for each dimension, which could be either int
    # or variable. In the latter case we'll use the extra neighborhood
    # argument to the function.
    ranges = []
    for i in range(the_array.ndim):
        if isinstance(kernel_size[i][0], int):
            lo = kernel_size[i][0]
            hi = kernel_size[i][1]
        else:
            lo = "{}[{}][0]".format(neighborhood_name, i)
            hi = "{}[{}][1]".format(neighborhood_name, i)
        ranges.append((lo, hi))

    # If there are more than one relatively indexed arrays, add a call to
    # a function that will raise an error if any of the relatively indexed
    # arrays are of different size than the first input array.
    if len(relatively_indexed) > 1:
        func_text += "    raise_if_incompatible_array_sizes(" + first_arg
        for other_array in relatively_indexed:
            if other_array != first_arg:
                func_text += "," + other_array
        func_text += ")\n"

    # Get the shape of the first input array.
    shape_name = ir_utils.get_unused_var_name("full_shape", name_var_table)
    func_text += "    {} = {}.shape\n".format(shape_name, first_arg)

    # If we have to allocate the output array (the out argument was not used)
    # then us numpy.full if the user specified a cval stencil decorator option
    # or np.zeros if they didn't to allocate the array.
    if result is None:
        if "cval" in self.options:
            cval = self.options["cval"]
            if return_type.dtype != typing.typeof.typeof(cval):
                raise ValueError("cval type does not match stencil return type.")

            out_init = "{} = np.full({}, {}, dtype=np.{})\n".format(
                out_name, shape_name, cval, return_type.dtype
            )

        else:
            out_init = "{} = np.zeros({}, dtype=np.{})\n".format(
                out_name, shape_name, return_type.dtype
            )
        func_text += "    " + out_init

    offset = 1
    # Add the loop nests to the new function.
    for i in range(the_array.ndim):
        for j in range(offset):
            func_text += "    "
        # ranges[i][0] is the minimum index used in the i'th dimension
        # but minimum's greater than 0 don't preclude any entry in the array.
        # So, take the minimum of 0 and the minimum index found in the kernel
        # and this will be a negative number (potentially -0).  Then, we do
        # unary - on that to get the positive offset in this dimension whose
        # use is precluded.
        # ranges[i][1] is the maximum of 0 and the observed maximum index
        # in this dimension because negative maximums would not cause us to
        # preclude any entry in the array from being used.
        func_text += ("for {} in range(-min(0,{}),{}[{}]-max(0,{})):\n").format(
            index_vars[i], ranges[i][0], shape_name, i, ranges[i][1]
        )
        offset += 1

    for j in range(offset):
        func_text += "    "
    # Put a sentinel in the code so we can locate it in the IR.  We will
    # remove this sentinel assignment and replace it with the IR for the
    # stencil kernel body.
    func_text += "{} = 0\n".format(sentinel_name)
    func_text += "    return {}\n".format(out_name)

    if config.DEBUG_ARRAY_OPT == 1:
        print("new stencil func text")
        print(func_text)

    # Force the new stencil function into existence.
    exec_(func_text) in globals(), locals()
    stencil_func = eval(stencil_func_name)
    if sigret is not None:
        pysig = utils.pysignature(stencil_func)
        sigret.pysig = pysig
    # Get the IR for the newly created stencil function.
    stencil_ir = compiler.run_frontend(stencil_func)
    ir_utils.remove_dels(stencil_ir.blocks)

    # rename all variables in stencil_ir afresh
    var_table = ir_utils.get_name_var_table(stencil_ir.blocks)
    new_var_dict = {}
    reserved_names = (
        [sentinel_name, out_name, neighborhood_name, shape_name]
        + kernel_copy.arg_names
        + index_vars
    )
    for name, var in var_table.items():
        if not name in reserved_names:
            new_var_dict[name] = ir_utils.mk_unique_var(name)
    ir_utils.replace_var_names(stencil_ir.blocks, new_var_dict)

    stencil_stub_last_label = max(stencil_ir.blocks.keys()) + 1

    # Shift lables in the kernel copy so they are guaranteed unique
    # and don't conflict with any labels in the stencil_ir.
    kernel_copy.blocks = ir_utils.add_offset_to_labels(
        kernel_copy.blocks, stencil_stub_last_label
    )
    new_label = max(kernel_copy.blocks.keys()) + 1
    # Adjust ret_blocks to account for addition of the offset.
    ret_blocks = [x + stencil_stub_last_label for x in ret_blocks]

    if config.DEBUG_ARRAY_OPT == 1:
        print("ret_blocks w/ offsets", ret_blocks, stencil_stub_last_label)
        print("before replace sentinel stencil_ir")
        ir_utils.dump_blocks(stencil_ir.blocks)
        print("before replace sentinel kernel_copy")
        ir_utils.dump_blocks(kernel_copy.blocks)

    # Search all the block in the stencil outline for the sentinel.
    for label, block in stencil_ir.blocks.items():
        for i, inst in enumerate(block.body):
            if isinstance(inst, ir.Assign) and inst.target.name == sentinel_name:
                # We found the sentinel assignment.
                loc = inst.loc
                scope = block.scope
                # split block across __sentinel__
                # A new block is allocated for the statements prior to the
                # sentinel but the new block maintains the current block
                # label.
                prev_block = ir.Block(scope, loc)
                prev_block.body = block.body[:i]
                # The current block is used for statements after sentinel.
                block.body = block.body[i + 1 :]
                # But the current block gets a new label.
                body_first_label = min(kernel_copy.blocks.keys())

                # The previous block jumps to the minimum labelled block of
                # the parfor body.
                prev_block.append(ir.Jump(body_first_label, loc))
                # Add all the parfor loop body blocks to the gufunc
                # function's IR.
                for l, b in kernel_copy.blocks.items():
                    stencil_ir.blocks[l] = b

                stencil_ir.blocks[new_label] = block
                stencil_ir.blocks[label] = prev_block
                # Add a jump from all the blocks that previously contained
                # a return in the stencil kernel to the block
                # containing statements after the sentinel.
                for ret_block in ret_blocks:
                    stencil_ir.blocks[ret_block].append(ir.Jump(new_label, loc))
                break
        else:
            continue
        break

    stencil_ir.blocks = ir_utils.rename_labels(stencil_ir.blocks)
    ir_utils.remove_dels(stencil_ir.blocks)

    assert isinstance(the_array, types.Type)
    array_types = args

    new_stencil_param_types = list(array_types)

    if config.DEBUG_ARRAY_OPT == 1:
        print("new_stencil_param_types", new_stencil_param_types)
        ir_utils.dump_blocks(stencil_ir.blocks)

    # Compile the combined stencil function with the replaced loop
    # body in it.
    new_func = compiler.compile_ir(
        self._typingctx,
        self._targetctx,
        stencil_ir,
        new_stencil_param_types,
        None,
        compiler.DEFAULT_FLAGS,
        {},
    )
    return new_func


# Traceback (numba.errors.TypingError)
$ python ml1.py
[[False False False False False]
[False False  True False False]
[False  True False  True False]
[False False False False False]]
Traceback (most recent call last):
File "ml1.py", line 16, in <module>
print(njit(foo)(A))
File "<path>/numba/numba/dispatcher.py", line 348, in _compile_for_args
error_rewrite(e, 'typing')
File "<path>/numba/numba/dispatcher.py", line 315, in error_rewrite
reraise(type(e), e, None)
File "<path>/numba/numba/six.py", line 658, in reraise
raise value.with_traceback(tb)
numba.errors.TypingError: Failed in nopython mode pipeline (step: nopython mode backend)
Failed in nopython mode pipeline (step: nopython frontend)
Invalid use of Function(<built-in function zeros>) with argument(s) of type(s): (tuple(int64 x 2), dtype=Function(<class 'bool'>))
* parameterized
In definition 0:
All templates rejected
This error is usually caused by passing an argument of a type that is unsupported by the named function.
[1] During: resolving callee type: Function(<built-in function zeros>)
[2] During: typing of call at <string> (3)


File "<string>", line 3:
<source missing, REPL/exec in use?>

[1] During: lowering "$0.3 = call $0.1(array, func=$0.1, args=[Var(array, ml1.py (13))], kws=(), vararg=None)" at ml1.py (13)

# Fixed code
def _stencil_wrapper(self, result, sigret, return_type, typemap, calltypes, *args):
    # Overall approach:
    # 1) Construct a string containing a function definition for the stencil function
    #    that will execute the stencil kernel.  This function definition includes a
    #    unique stencil function name, the parameters to the stencil kernel, loop
    #    nests across the dimenions of the input array.  Those loop nests use the
    #    computed stencil kernel size so as not to try to compute elements where
    #    elements outside the bounds of the input array would be needed.
    # 2) The but of the loop nest in this new function is a special sentinel
    #    assignment.
    # 3) Get the IR of this new function.
    # 4) Split the block containing the sentinel assignment and remove the sentinel
    #    assignment.  Insert the stencil kernel IR into the stencil function IR
    #    after label and variable renaming of the stencil kernel IR to prevent
    #    conflicts with the stencil function IR.
    # 5) Compile the combined stencil function IR + stencil kernel IR into existence.

    # Copy the kernel so that our changes for this callsite
    # won't effect other callsites.
    (kernel_copy, copy_calltypes) = self.copy_ir_with_calltypes(
        self.kernel_ir, calltypes
    )
    # The stencil kernel body becomes the body of a loop, for which args aren't needed.
    ir_utils.remove_args(kernel_copy.blocks)
    first_arg = kernel_copy.arg_names[0]

    in_cps, out_cps = ir_utils.copy_propagate(kernel_copy.blocks, typemap)
    name_var_table = ir_utils.get_name_var_table(kernel_copy.blocks)
    ir_utils.apply_copy_propagate(
        kernel_copy.blocks, in_cps, name_var_table, typemap, copy_calltypes
    )

    if "out" in name_var_table:
        raise ValueError("Cannot use the reserved word 'out' in stencil kernels.")

    sentinel_name = ir_utils.get_unused_var_name("__sentinel__", name_var_table)
    if config.DEBUG_ARRAY_OPT == 1:
        print("name_var_table", name_var_table, sentinel_name)

    the_array = args[0]

    if config.DEBUG_ARRAY_OPT == 1:
        print(
            "_stencil_wrapper",
            return_type,
            return_type.dtype,
            type(return_type.dtype),
            args,
        )
        ir_utils.dump_blocks(kernel_copy.blocks)

    # We generate a Numba function to execute this stencil and here
    # create the unique name of this function.
    stencil_func_name = "__numba_stencil_%s_%s" % (
        hex(id(the_array)).replace("-", "_"),
        self.id,
    )

    # We will put a loop nest in the generated function for each
    # dimension in the input array.  Here we create the name for
    # the index variable for each dimension.  index0, index1, ...
    index_vars = []
    for i in range(the_array.ndim):
        index_var_name = ir_utils.get_unused_var_name("index" + str(i), name_var_table)
        index_vars += [index_var_name]

    # Create extra signature for out and neighborhood.
    out_name = ir_utils.get_unused_var_name("out", name_var_table)
    neighborhood_name = ir_utils.get_unused_var_name("neighborhood", name_var_table)
    sig_extra = ""
    if result is not None:
        sig_extra += ", {}=None".format(out_name)
    if "neighborhood" in dict(self.kws):
        sig_extra += ", {}=None".format(neighborhood_name)

    # Get a list of the standard indexed array names.
    standard_indexed = self.options.get("standard_indexing", [])

    if first_arg in standard_indexed:
        raise ValueError(
            "The first argument to a stencil kernel must "
            "use relative indexing, not standard indexing."
        )

    if len(set(standard_indexed) - set(kernel_copy.arg_names)) != 0:
        raise ValueError(
            "Standard indexing requested for an array name "
            "not present in the stencil kernel definition."
        )

    # Add index variables to getitems in the IR to transition the accesses
    # in the kernel from relative to regular Python indexing.  Returns the
    # computed size of the stencil kernel and a list of the relatively indexed
    # arrays.
    kernel_size, relatively_indexed = self.add_indices_to_kernel(
        kernel_copy, index_vars, the_array.ndim, self.neighborhood, standard_indexed
    )
    if self.neighborhood is None:
        self.neighborhood = kernel_size

    if config.DEBUG_ARRAY_OPT == 1:
        print("After add_indices_to_kernel")
        ir_utils.dump_blocks(kernel_copy.blocks)

    # The return in the stencil kernel becomes a setitem for that
    # particular point in the iteration space.
    ret_blocks = self.replace_return_with_setitem(
        kernel_copy.blocks, index_vars, out_name
    )

    if config.DEBUG_ARRAY_OPT == 1:
        print("After replace_return_with_setitem", ret_blocks)
        ir_utils.dump_blocks(kernel_copy.blocks)

    # Start to form the new function to execute the stencil kernel.
    func_text = "def {}({}{}):\n".format(
        stencil_func_name, ",".join(kernel_copy.arg_names), sig_extra
    )

    # Get loop ranges for each dimension, which could be either int
    # or variable. In the latter case we'll use the extra neighborhood
    # argument to the function.
    ranges = []
    for i in range(the_array.ndim):
        if isinstance(kernel_size[i][0], int):
            lo = kernel_size[i][0]
            hi = kernel_size[i][1]
        else:
            lo = "{}[{}][0]".format(neighborhood_name, i)
            hi = "{}[{}][1]".format(neighborhood_name, i)
        ranges.append((lo, hi))

    # If there are more than one relatively indexed arrays, add a call to
    # a function that will raise an error if any of the relatively indexed
    # arrays are of different size than the first input array.
    if len(relatively_indexed) > 1:
        func_text += "    raise_if_incompatible_array_sizes(" + first_arg
        for other_array in relatively_indexed:
            if other_array != first_arg:
                func_text += "," + other_array
        func_text += ")\n"

    # Get the shape of the first input array.
    shape_name = ir_utils.get_unused_var_name("full_shape", name_var_table)
    func_text += "    {} = {}.shape\n".format(shape_name, first_arg)

    # If we have to allocate the output array (the out argument was not used)
    # then us numpy.full if the user specified a cval stencil decorator option
    # or np.zeros if they didn't to allocate the array.
    if result is None:
        return_type_name = numpy_support.as_dtype(return_type.dtype).type.__name__
        if "cval" in self.options:
            cval = self.options["cval"]
            if return_type.dtype != typing.typeof.typeof(cval):
                raise ValueError("cval type does not match stencil return type.")
            out_init = "{} = np.full({}, {}, dtype=np.{})\n".format(
                out_name, shape_name, cval, return_type_name
            )
        else:
            out_init = "{} = np.zeros({}, dtype=np.{})\n".format(
                out_name, shape_name, return_type_name
            )
        func_text += "    " + out_init

    offset = 1
    # Add the loop nests to the new function.
    for i in range(the_array.ndim):
        for j in range(offset):
            func_text += "    "
        # ranges[i][0] is the minimum index used in the i'th dimension
        # but minimum's greater than 0 don't preclude any entry in the array.
        # So, take the minimum of 0 and the minimum index found in the kernel
        # and this will be a negative number (potentially -0).  Then, we do
        # unary - on that to get the positive offset in this dimension whose
        # use is precluded.
        # ranges[i][1] is the maximum of 0 and the observed maximum index
        # in this dimension because negative maximums would not cause us to
        # preclude any entry in the array from being used.
        func_text += ("for {} in range(-min(0,{}),{}[{}]-max(0,{})):\n").format(
            index_vars[i], ranges[i][0], shape_name, i, ranges[i][1]
        )
        offset += 1

    for j in range(offset):
        func_text += "    "
    # Put a sentinel in the code so we can locate it in the IR.  We will
    # remove this sentinel assignment and replace it with the IR for the
    # stencil kernel body.
    func_text += "{} = 0\n".format(sentinel_name)
    func_text += "    return {}\n".format(out_name)

    if config.DEBUG_ARRAY_OPT == 1:
        print("new stencil func text")
        print(func_text)

    # Force the new stencil function into existence.
    exec_(func_text) in globals(), locals()
    stencil_func = eval(stencil_func_name)
    if sigret is not None:
        pysig = utils.pysignature(stencil_func)
        sigret.pysig = pysig
    # Get the IR for the newly created stencil function.
    stencil_ir = compiler.run_frontend(stencil_func)
    ir_utils.remove_dels(stencil_ir.blocks)

    # rename all variables in stencil_ir afresh
    var_table = ir_utils.get_name_var_table(stencil_ir.blocks)
    new_var_dict = {}
    reserved_names = (
        [sentinel_name, out_name, neighborhood_name, shape_name]
        + kernel_copy.arg_names
        + index_vars
    )
    for name, var in var_table.items():
        if not name in reserved_names:
            new_var_dict[name] = ir_utils.mk_unique_var(name)
    ir_utils.replace_var_names(stencil_ir.blocks, new_var_dict)

    stencil_stub_last_label = max(stencil_ir.blocks.keys()) + 1

    # Shift lables in the kernel copy so they are guaranteed unique
    # and don't conflict with any labels in the stencil_ir.
    kernel_copy.blocks = ir_utils.add_offset_to_labels(
        kernel_copy.blocks, stencil_stub_last_label
    )
    new_label = max(kernel_copy.blocks.keys()) + 1
    # Adjust ret_blocks to account for addition of the offset.
    ret_blocks = [x + stencil_stub_last_label for x in ret_blocks]

    if config.DEBUG_ARRAY_OPT == 1:
        print("ret_blocks w/ offsets", ret_blocks, stencil_stub_last_label)
        print("before replace sentinel stencil_ir")
        ir_utils.dump_blocks(stencil_ir.blocks)
        print("before replace sentinel kernel_copy")
        ir_utils.dump_blocks(kernel_copy.blocks)

    # Search all the block in the stencil outline for the sentinel.
    for label, block in stencil_ir.blocks.items():
        for i, inst in enumerate(block.body):
            if isinstance(inst, ir.Assign) and inst.target.name == sentinel_name:
                # We found the sentinel assignment.
                loc = inst.loc
                scope = block.scope
                # split block across __sentinel__
                # A new block is allocated for the statements prior to the
                # sentinel but the new block maintains the current block
                # label.
                prev_block = ir.Block(scope, loc)
                prev_block.body = block.body[:i]
                # The current block is used for statements after sentinel.
                block.body = block.body[i + 1 :]
                # But the current block gets a new label.
                body_first_label = min(kernel_copy.blocks.keys())

                # The previous block jumps to the minimum labelled block of
                # the parfor body.
                prev_block.append(ir.Jump(body_first_label, loc))
                # Add all the parfor loop body blocks to the gufunc
                # function's IR.
                for l, b in kernel_copy.blocks.items():
                    stencil_ir.blocks[l] = b

                stencil_ir.blocks[new_label] = block
                stencil_ir.blocks[label] = prev_block
                # Add a jump from all the blocks that previously contained
                # a return in the stencil kernel to the block
                # containing statements after the sentinel.
                for ret_block in ret_blocks:
                    stencil_ir.blocks[ret_block].append(ir.Jump(new_label, loc))
                break
        else:
            continue
        break

    stencil_ir.blocks = ir_utils.rename_labels(stencil_ir.blocks)
    ir_utils.remove_dels(stencil_ir.blocks)

    assert isinstance(the_array, types.Type)
    array_types = args

    new_stencil_param_types = list(array_types)

    if config.DEBUG_ARRAY_OPT == 1:
        print("new_stencil_param_types", new_stencil_param_types)
        ir_utils.dump_blocks(stencil_ir.blocks)

    # Compile the combined stencil function with the replaced loop
    # body in it.
    new_func = compiler.compile_ir(
        self._typingctx,
        self._targetctx,
        stencil_ir,
        new_stencil_param_types,
        None,
        compiler.DEFAULT_FLAGS,
        {},
    )
    return new_func