Repository: numba/numba

Issue:
tuple slicing fails in inlined functions
Before submitting a bug report please ensure that you can check off these boxes:

- [x] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).

0.46.0

- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

Here is the reproducer:


```python
import numpy as np
from numba import njit, types
from numba.extending import overload


def output_factory(array, dtype):
    pass


@overload(output_factory, inline='always')
def ol_output_factory(array, dtype):
    if isinstance(array, types.npytypes.Array):
        def impl(array, dtype):
            shape = array.shape[3:]
            return np.zeros(shape, dtype=dtype)

        return impl


@njit(nogil=True)
def fn(array):
    out = output_factory(array, array.dtype)
    return out


@njit(nogil=True)
def fn2(array):
    return np.zeros(array.shape[3:], dtype=array.dtype)


fn(np.ones((10, 20, 30, 40, 50)))

fn2(np.ones((10, 20, 30, 40, 50)))
```

The call to ``fn`` fails with the following:

```traceback
Traceback (most recent call last):
  File "test_inlining.py", line 30, in <module>
    fn(np.ones((10, 20, 30, 40, 50)))
  File "/home/sperkins/venv/afr3/lib/python3.6/site-packages/numba/dispatcher.py", line 401, in _compile_for_args
    error_rewrite(e, 'typing')
  File "/home/sperkins/venv/afr3/lib/python3.6/site-packages/numba/dispatcher.py", line 344, in error_rewrite
    reraise(type(e), e, None)
  File "/home/sperkins/venv/afr3/lib/python3.6/site-packages/numba/six.py", line 668, in reraise
    raise value.with_traceback(tb)
numba.errors.TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Invalid use of Function(<function output_factory at 0x7f9f5ac4ce18>) with argument(s) of type(s): (array(float64, 5d, C), dtype(float64))
 * parameterized
In definition 0:
    TypingError: Invalid use of Function(<built-in function getitem>) with argument(s) of type(s): (tuple(int64 x 5), slice<a:b>)
 * parameterized
In definition 0:
    All templates rejected with literals.
In definition 1:
    All templates rejected without literals.
In definition 2:
    All templates rejected with literals.
In definition 3:
    All templates rejected without literals.
In definition 4:
    All templates rejected with literals.
In definition 5:
    All templates rejected without literals.
In definition 6:
    All templates rejected with literals.
In definition 7:
    All templates rejected without literals.
In definition 8:
    All templates rejected with literals.
In definition 9:
    All templates rejected without literals.
In definition 10:
    All templates rejected with literals.
In definition 11:
    All templates rejected without literals.
In definition 12:
    All templates rejected with literals.
In definition 13:
    All templates rejected without literals.
This error is usually caused by passing an argument of a type that is unsupported by the named function.
[1] During: typing of intrinsic-call at test_inlining.py (14)

File "test_inlining.py", line 14:
        def impl(array, dtype):
            shape = array.shape[3:]
            ^

    raised from /home/sperkins/venv/afr3/lib/python3.6/site-packages/numba/typeinfer.py:951
In definition 1:
    TypingError: Invalid use of Function(<built-in function getitem>) with argument(s) of type(s): (tuple(int64 x 5), slice<a:b>)
 * parameterized
In definition 0:
    All templates rejected with literals.
In definition 1:
    All templates rejected without literals.
In definition 2:
    All templates rejected with literals.
In definition 3:
    All templates rejected without literals.
In definition 4:
    All templates rejected with literals.
In definition 5:
    All templates rejected without literals.
In definition 6:
    All templates rejected with literals.
In definition 7:
    All templates rejected without literals.
In definition 8:
    All templates rejected with literals.
In definition 9:
    All templates rejected without literals.
In definition 10:
    All templates rejected with literals.
In definition 11:
    All templates rejected without literals.
In definition 12:
    All templates rejected with literals.
In definition 13:
    All templates rejected without literals.
This error is usually caused by passing an argument of a type that is unsupported by the named function.
[1] During: typing of intrinsic-call at test_inlining.py (14)

File "test_inlining.py", line 14:
        def impl(array, dtype):
            shape = array.shape[3:]
            ^

    raised from /home/sperkins/venv/afr3/lib/python3.6/site-packages/numba/typeinfer.py:951
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(<function output_factory at 0x7f9f5ac4ce18>)
[2] During: typing of call at test_inlining.py (22)


File "test_inlining.py", line 22:
def fn(array):
    out = output_factory(array, array.dtype)
    ^

This is not usually a problem with Numba itself but instead often caused by
the use of unsupported features or an issue in resolving types.

To see Python/NumPy features supported by the latest release of Numba visit:
http://numba.pydata.org/numba-doc/latest/reference/pysupported.html
and
http://numba.pydata.org/numba-doc/latest/reference/numpysupported.html

For more information about typing errors and how to debug them visit:
http://numba.pydata.org/numba-doc/latest/user/troubleshoot.html#my-code-doesn-t-compile

If you think your code should work with Numba, please report the error message
and traceback, along with a minimal reproducer at:
https://github.com/numba/numba/issues/new
```

but the call to ``fn2`` succeeds.

I know the ``inline`` feature is new. Am I perhaps using it incorrectly?

Discussion:

Patch:
diff --git a/numba/core/compiler.py b/numba/core/compiler.py
index 13ad9ec82..4962e19ae 100644
--- a/numba/core/compiler.py
+++ b/numba/core/compiler.py
@@ -435,11 +435,68 @@ class DefaultPassBuilder(object):
       - nopython
       - objectmode
       - interpreted
+      - typed
+      - untyped
+      - nopython lowering
     """
     @staticmethod
     def define_nopython_pipeline(state, name='nopython'):
         """Returns an nopython mode pipeline based PassManager
         """
+        # compose pipeline from untyped, typed and lowering parts
+        dpb = DefaultPassBuilder
+        pm = PassManager(name)
+        untyped_passes = dpb.define_untyped_pipeline(state)
+        pm.passes.extend(untyped_passes.passes)
+
+        typed_passes = dpb.define_typed_pipeline(state)
+        pm.passes.extend(typed_passes.passes)
+
+        lowering_passes = dpb.define_nopython_lowering_pipeline(state)
+        pm.passes.extend(lowering_passes.passes)
+
+        pm.finalize()
+        return pm
+
+    @staticmethod
+    def define_nopython_lowering_pipeline(state, name='nopython_lowering'):
+        pm = PassManager(name)
+        # legalise
+        pm.add_pass(IRLegalization,
+                    "ensure IR is legal prior to lowering")
+
+        # lower
+        pm.add_pass(NoPythonBackend, "nopython mode backend")
+        pm.add_pass(DumpParforDiagnostics, "dump parfor diagnostics")
+        pm.finalize()
+        return pm
+
+    @staticmethod
+    def define_typed_pipeline(state, name="typed"):
+        """Returns the typed part of the nopython pipeline"""
+        pm = PassManager(name)
+        # typing
+        pm.add_pass(NopythonTypeInference, "nopython frontend")
+        pm.add_pass(AnnotateTypes, "annotate types")
+
+        # strip phis
+        pm.add_pass(PreLowerStripPhis, "remove phis nodes")
+
+        # optimisation
+        pm.add_pass(InlineOverloads, "inline overloaded functions")
+        if state.flags.auto_parallel.enabled:
+            pm.add_pass(PreParforPass, "Preprocessing for parfors")
+        if not state.flags.no_rewrites:
+            pm.add_pass(NopythonRewrites, "nopython rewrites")
+        if state.flags.auto_parallel.enabled:
+            pm.add_pass(ParforPass, "convert to parfors")
+
+        pm.finalize()
+        return pm
+
+    @staticmethod
+    def define_untyped_pipeline(state, name='untyped'):
+        """Returns an untyped part of the nopython pipeline"""
         pm = PassManager(name)
         if state.func_ir is None:
             pm.add_pass(TranslateByteCode, "analyzing bytecode")
@@ -471,29 +528,7 @@ class DefaultPassBuilder(object):
 
         if state.flags.enable_ssa:
             pm.add_pass(ReconstructSSA, "ssa")
-        # typing
-        pm.add_pass(NopythonTypeInference, "nopython frontend")
-        pm.add_pass(AnnotateTypes, "annotate types")
 
-        # strip phis
-        pm.add_pass(PreLowerStripPhis, "remove phis nodes")
-
-        # optimisation
-        pm.add_pass(InlineOverloads, "inline overloaded functions")
-        if state.flags.auto_parallel.enabled:
-            pm.add_pass(PreParforPass, "Preprocessing for parfors")
-        if not state.flags.no_rewrites:
-            pm.add_pass(NopythonRewrites, "nopython rewrites")
-        if state.flags.auto_parallel.enabled:
-            pm.add_pass(ParforPass, "convert to parfors")
-
-        # legalise
-        pm.add_pass(IRLegalization,
-                    "ensure IR is legal prior to lowering")
-
-        # lower
-        pm.add_pass(NoPythonBackend, "nopython mode backend")
-        pm.add_pass(DumpParforDiagnostics, "dump parfor diagnostics")
         pm.finalize()
         return pm
 
diff --git a/numba/core/inline_closurecall.py b/numba/core/inline_closurecall.py
index 7c8b16865..b73c22a66 100644
--- a/numba/core/inline_closurecall.py
+++ b/numba/core/inline_closurecall.py
@@ -122,6 +122,13 @@ class InlineClosureCallPass(object):
                 _fix_nested_array(self.func_ir)
 
         if modified:
+            # clean up now dead/unreachable blocks, e.g. unconditionally raising
+            # an exception in an inlined function would render some parts of the
+            # inliner unreachable
+            cfg = compute_cfg_from_blocks(self.func_ir.blocks)
+            for dead in cfg.dead_nodes():
+                del self.func_ir.blocks[dead]
+
             # run dead code elimination
             dead_code_elimination(self.func_ir)
             # do label renaming
@@ -262,6 +269,247 @@ def check_reduce_func(func_ir, func_var):
     return reduce_func
 
 
+class InlineWorker(object):
+    """ A worker class for inlining, this is a more advanced version of
+    `inline_closure_call` in that it permits inlining from function type, Numba
+    IR and code object. It also, runs the entire untyped compiler pipeline on
+    the inlinee to ensure that it is transformed as though it were compiled
+    directly.
+    """
+
+    def __init__(self,
+                 typingctx=None,
+                 targetctx=None,
+                 locals=None,
+                 pipeline=None,
+                 flags=None,
+                 validator=callee_ir_validator,
+                 typemap=None,
+                 calltypes=None):
+        """
+        Instantiate a new InlineWorker, all arguments are optional though some
+        must be supplied together for certain use cases. The methods will refuse
+        to run if the object isn't configured in the manner needed. Args are the
+        same as those in a numba.core.Compiler.state, except the validator which
+        is a function taking Numba IR and validating it for use when inlining
+        (this is optional and really to just provide better error messages about
+        things which the inliner cannot handle like yield in closure).
+        """
+        def check(arg, name):
+            if arg is None:
+                raise TypeError("{} must not be None".format(name))
+
+        from numba.core.compiler import DefaultPassBuilder
+
+        # check the stuff needed to run the more advanced compilation pipeline
+        # is valid if any of it is provided
+        compiler_args = (targetctx, locals, pipeline, flags)
+        compiler_group = [x is not None for x in compiler_args]
+        if any(compiler_group) and not all(compiler_group):
+            check(targetctx, 'targetctx')
+            check(locals, 'locals')
+            check(pipeline, 'pipeline')
+            check(flags, 'flags')
+        elif all(compiler_group):
+            check(typingctx, 'typingctx')
+
+        self._compiler_pipeline = DefaultPassBuilder.define_untyped_pipeline
+
+        self.typingctx = typingctx
+        self.targetctx = targetctx
+        self.locals = locals
+        self.pipeline = pipeline
+        self.flags = flags
+        self.validator = validator
+        self.debug_print = _make_debug_print("InlineWorker")
+
+        # check whether this inliner can also support typemap and calltypes
+        # update and if what's provided is valid
+        pair = (typemap, calltypes)
+        pair_is_none = [x is None for x in pair]
+        if any(pair_is_none) and not all(pair_is_none):
+            msg = ("typemap and calltypes must both be either None or have a "
+                   "value, got: %s, %s")
+            raise TypeError(msg % pair)
+        self._permit_update_type_and_call_maps = not all(pair_is_none)
+        self.typemap = typemap
+        self.calltypes = calltypes
+
+
+    def inline_ir(self, caller_ir, block, i, callee_ir, callee_freevars,
+                  arg_typs=None):
+        """ Inlines the callee_ir in the caller_ir at statement index i of block
+        `block`, callee_freevars are the free variables for the callee_ir. If
+        the callee_ir is derived from a function `func` then this is
+        `func.__code__.co_freevars`. If `arg_typs` is given and the InlineWorker
+        instance was initialized with a typemap and calltypes then they will be
+        appropriately updated based on the arg_typs.
+        """
+        # check that the contents of the callee IR is something that can be
+        # inlined if a validator is present
+        if self.validator is not None:
+            self.validator(callee_ir)
+
+        # save an unmutated copy of the callee_ir to return
+        callee_ir_original = callee_ir.copy()
+        scope = block.scope
+        instr = block.body[i]
+        call_expr = instr.value
+        callee_blocks = callee_ir.blocks
+
+        # 1. relabel callee_ir by adding an offset
+        max_label = max(ir_utils._max_label, max(caller_ir.blocks.keys()))
+        callee_blocks = add_offset_to_labels(callee_blocks, max_label + 1)
+        callee_blocks = simplify_CFG(callee_blocks)
+        callee_ir.blocks = callee_blocks
+        min_label = min(callee_blocks.keys())
+        max_label = max(callee_blocks.keys())
+        #    reset globals in ir_utils before we use it
+        ir_utils._max_label = max_label
+        self.debug_print("After relabel")
+        _debug_dump(callee_ir)
+
+        # 2. rename all local variables in callee_ir with new locals created in
+        # caller_ir
+        callee_scopes = _get_all_scopes(callee_blocks)
+        self.debug_print("callee_scopes = ", callee_scopes)
+        #    one function should only have one local scope
+        assert(len(callee_scopes) == 1)
+        callee_scope = callee_scopes[0]
+        var_dict = {}
+        for var in callee_scope.localvars._con.values():
+            if not (var.name in callee_freevars):
+                new_var = scope.redefine(mk_unique_var(var.name), loc=var.loc)
+                var_dict[var.name] = new_var
+        self.debug_print("var_dict = ", var_dict)
+        replace_vars(callee_blocks, var_dict)
+        self.debug_print("After local var rename")
+        _debug_dump(callee_ir)
+
+        # 3. replace formal parameters with actual arguments
+        callee_func = callee_ir.func_id.func
+        args = _get_callee_args(call_expr, callee_func, block.body[i].loc,
+                                caller_ir)
+
+        # 4. Update typemap
+        if self._permit_update_type_and_call_maps:
+            if arg_typs is None:
+                raise TypeError('arg_typs should have a value not None')
+            self.update_type_and_call_maps(callee_ir, arg_typs)
+
+        self.debug_print("After arguments rename: ")
+        _debug_dump(callee_ir)
+
+        _replace_args_with(callee_blocks, args)
+        # 5. split caller blocks into two
+        new_blocks = []
+        new_block = ir.Block(scope, block.loc)
+        new_block.body = block.body[i + 1:]
+        new_label = next_label()
+        caller_ir.blocks[new_label] = new_block
+        new_blocks.append((new_label, new_block))
+        block.body = block.body[:i]
+        block.body.append(ir.Jump(min_label, instr.loc))
+
+        # 6. replace Return with assignment to LHS
+        topo_order = find_topo_order(callee_blocks)
+        _replace_returns(callee_blocks, instr.target, new_label)
+
+        # remove the old definition of instr.target too
+        if (instr.target.name in caller_ir._definitions
+                and call_expr in caller_ir._definitions[instr.target.name]):
+            # NOTE: target can have multiple definitions due to control flow
+            caller_ir._definitions[instr.target.name].remove(call_expr)
+
+        # 7. insert all new blocks, and add back definitions
+        for label in topo_order:
+            # block scope must point to parent's
+            block = callee_blocks[label]
+            block.scope = scope
+            _add_definitions(caller_ir, block)
+            caller_ir.blocks[label] = block
+            new_blocks.append((label, block))
+        self.debug_print("After merge in")
+        _debug_dump(caller_ir)
+
+        return callee_ir_original, callee_blocks, var_dict, new_blocks
+
+    def inline_function(self, caller_ir, block, i, function, arg_typs=None):
+        """ Inlines the function in the caller_ir at statement index i of block
+        `block`. If `arg_typs` is given and the InlineWorker instance was
+        initialized with a typemap and calltypes then they will be appropriately
+        updated based on the arg_typs.
+        """
+        callee_ir = self.run_untyped_passes(function)
+        freevars = function.__code__.co_freevars
+        return self.inline_ir(caller_ir, block, i, callee_ir, freevars,
+                              arg_typs=arg_typs)
+
+    def run_untyped_passes(self, func):
+        """
+        Run the compiler frontend's untyped passes over the given Python
+        function, and return the function's canonical Numba IR.
+        """
+        from numba.core.compiler import StateDict, _CompileStatus
+        from numba.core.untyped_passes import ExtractByteCode, WithLifting
+        from numba.core import bytecode
+        from numba.parfors.parfor import ParforDiagnostics
+        state = StateDict()
+        state.func_ir = None
+        state.typingctx = self.typingctx
+        state.targetctx = self.targetctx
+        state.locals = self.locals
+        state.pipeline = self.pipeline
+        state.flags = self.flags
+
+        # Disable SSA transformation, the call site won't be in SSA form and
+        # self.inline_ir depends on this being the case.
+        state.flags.enable_ssa = False
+
+        state.func_id = bytecode.FunctionIdentity.from_function(func)
+
+        state.typemap = None
+        state.calltypes = None
+        state.type_annotation = None
+        state.status = _CompileStatus(False, False)
+        state.return_type = None
+        state.parfor_diagnostics = ParforDiagnostics()
+        state.metadata = {}
+
+        ExtractByteCode().run_pass(state)
+        # This is a lie, just need *some* args for the case where an obj mode
+        # with lift is needed
+        state.args = len(state.bc.func_id.pysig.parameters) * (types.pyobject,)
+
+        pm = self._compiler_pipeline(state)
+
+        pm.finalize()
+        pm.run(state)
+        return state.func_ir
+
+    def update_type_and_call_maps(self, callee_ir, arg_typs):
+        """ Updates the type and call maps based on calling callee_ir with arguments
+        from arg_typs"""
+        if not self._permit_update_type_and_call_maps:
+            msg = ("InlineWorker instance not configured correctly, typemap or "
+                   "calltypes missing in initialization.")
+            raise ValueError(msg)
+        from numba.core import typed_passes
+        # call branch pruning to simplify IR and avoid inference errors
+        callee_ir._definitions = ir_utils.build_definitions(callee_ir.blocks)
+        numba.core.analysis.dead_branch_prune(callee_ir, arg_typs)
+        f_typemap, f_return_type, f_calltypes = typed_passes.type_inference_stage(
+                self.typingctx, callee_ir, arg_typs, None)
+        canonicalize_array_math(callee_ir, f_typemap,
+                                f_calltypes, self.typingctx)
+        # remove argument entries like arg.a from typemap
+        arg_names = [vname for vname in f_typemap if vname.startswith("arg.")]
+        for a in arg_names:
+            f_typemap.pop(a)
+        self.typemap.update(f_typemap)
+        self.calltypes.update(f_calltypes)
+
+
 def inline_closure_call(func_ir, glbls, block, i, callee, typingctx=None,
                         arg_typs=None, typemap=None, calltypes=None,
                         work_list=None, callee_validator=None,
diff --git a/numba/core/ir.py b/numba/core/ir.py
index b6da59bfb..0b93fbabd 100644
--- a/numba/core/ir.py
+++ b/numba/core/ir.py
@@ -725,11 +725,11 @@ class StaticRaise(Terminator):
 
     def __str__(self):
         if self.exc_class is None:
-            return "raise"
+            return "<static> raise"
         elif self.exc_args is None:
-            return "raise %s" % (self.exc_class,)
+            return "<static> raise %s" % (self.exc_class,)
         else:
-            return "raise %s(%s)" % (self.exc_class,
+            return "<static> raise %s(%s)" % (self.exc_class,
                                      ", ".join(map(repr, self.exc_args)))
 
     def get_targets(self):
diff --git a/numba/core/typed_passes.py b/numba/core/typed_passes.py
index 691996c40..25d8f7a91 100644
--- a/numba/core/typed_passes.py
+++ b/numba/core/typed_passes.py
@@ -17,7 +17,7 @@ from numba.core.ir_utils import (raise_on_unsupported_feature, warn_deprecated,
                                  check_and_legalize_ir, guard,
                                  dead_code_elimination, simplify_CFG,
                                  get_definition, remove_dels,
-                                 build_definitions)
+                                 build_definitions, compute_cfg_from_blocks)
 from numba.core import postproc
 
 
@@ -487,8 +487,20 @@ class InlineOverloads(FunctionPass):
         """
         if self._DEBUG:
             print('before overload inline'.center(80, '-'))
+            print(state.func_id.unique_name)
             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,
+                                     callee_ir_validator,
+                                     state.typemap,
+                                     state.calltypes,
+                                     )
         modified = False
         work_list = list(state.func_ir.blocks.items())
         # use a work list, look for call sites via `ir.Expr.op == call` and
@@ -506,16 +518,22 @@ class InlineOverloads(FunctionPass):
                         else:
                             continue
 
-                        if guard(workfn, state, work_list, block, i, expr):
+                        if guard(workfn, state, work_list, block, i, expr,
+                                 inline_worker):
                             modified = True
                             break  # because block structure changed
 
         if self._DEBUG:
             print('after overload inline'.center(80, '-'))
+            print(state.func_id.unique_name)
             print(state.func_ir.dump())
             print(''.center(80, '-'))
 
         if modified:
+            # Remove dead blocks, this is safe as it relies on the CFG only.
+            cfg = compute_cfg_from_blocks(state.func_ir.blocks)
+            for dead in cfg.dead_nodes():
+                del state.func_ir.blocks[dead]
             # clean up blocks
             dead_code_elimination(state.func_ir,
                                   typemap=state.type_annotation.typemap)
@@ -525,12 +543,12 @@ class InlineOverloads(FunctionPass):
 
         if self._DEBUG:
             print('after overload inline DCE'.center(80, '-'))
+            print(state.func_id.unique_name)
             print(state.func_ir.dump())
             print(''.center(80, '-'))
-
         return True
 
-    def _do_work_getattr(self, state, work_list, block, i, expr):
+    def _do_work_getattr(self, state, work_list, block, i, expr, inline_worker):
         recv_type = state.type_annotation.typemap[expr.value.name]
         recv_type = types.unliteral(recv_type)
         matched = state.typingctx.find_matching_getattr_template(
@@ -564,10 +582,10 @@ class InlineOverloads(FunctionPass):
         is_method = False
         return self._run_inliner(
             state, inline_type, sig, template, arg_typs, expr, i, impl, block,
-            work_list, is_method,
+            work_list, is_method, inline_worker,
         )
 
-    def _do_work_call(self, state, work_list, block, i, expr):
+    def _do_work_call(self, state, work_list, block, i, expr, inline_worker):
         # 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
@@ -627,15 +645,13 @@ class InlineOverloads(FunctionPass):
         # definitely something that could be inlined.
         return self._run_inliner(
             state, inline_type, sig, template, arg_typs, expr, i, impl, block,
-            work_list, is_method,
+            work_list, is_method, inline_worker,
         )
 
     def _run_inliner(
         self, state, inline_type, sig, template, arg_typs, expr, i, impl, block,
-        work_list, is_method,
+        work_list, is_method, inline_worker,
     ):
-        from numba.core.inline_closurecall import (inline_closure_call,
-                                                   callee_ir_validator)
 
         do_inline = True
         if not inline_type.is_always_inline:
@@ -657,15 +673,17 @@ class InlineOverloads(FunctionPass):
                 if not self._add_method_self_arg(state, expr):
                     return False
             arg_typs = template._inline_overloads[arg_typs]['folded_args']
-            # pass is typed so use the callee globals
-            inline_closure_call(state.func_ir, impl.__globals__,
-                                block, i, impl, typingctx=state.typingctx,
-                                arg_typs=arg_typs,
-                                typemap=state.type_annotation.typemap,
-                                calltypes=state.type_annotation.calltypes,
-                                work_list=work_list,
-                                replace_freevars=False,
-                                callee_validator=callee_ir_validator)
+            iinfo = template._inline_overloads[arg_typs]['iinfo']
+            freevars = iinfo.func_ir.func_id.func.__code__.co_freevars
+            _, _, _, new_blocks = inline_worker.inline_ir(state.func_ir,
+                                                          block,
+                                                          i,
+                                                          iinfo.func_ir,
+                                                          freevars,
+                                                          arg_typs=arg_typs)
+            if work_list is not None:
+                for blk in new_blocks:
+                    work_list.append(blk)
             return True
         else:
             return False
diff --git a/numba/core/typing/templates.py b/numba/core/typing/templates.py
index 4332773f5..4370afdad 100644
--- a/numba/core/typing/templates.py
+++ b/numba/core/typing/templates.py
@@ -500,7 +500,24 @@ class _OverloadFunctionTemplate(AbstractTemplate):
             # need to run the compiler front end up to type inference to compute
             # a signature
             from numba.core import typed_passes, compiler
-            ir = compiler.run_frontend(disp_type.dispatcher.py_func)
+            from numba.core.inline_closurecall import InlineWorker
+            fcomp = disp._compiler
+            flags = compiler.Flags()
+
+            # Updating these causes problems?!
+            #fcomp.targetdescr.options.parse_as_flags(flags,
+            #                                         fcomp.targetoptions)
+            #flags = fcomp._customize_flags(flags)
+
+            # spoof a compiler pipline like the one that will be in use
+            tyctx = fcomp.targetdescr.typing_context
+            tgctx = fcomp.targetdescr.target_context
+            compiler_inst = fcomp.pipeline_class(tyctx, tgctx, None, None, None,
+                                                 flags, None, )
+            inline_worker = InlineWorker(tyctx, tgctx, fcomp.locals,
+                                         compiler_inst, flags, None,)
+
+            ir = inline_worker.run_untyped_passes(disp_type.dispatcher.py_func)
             resolve = disp_type.dispatcher.get_call_template
             template, pysig, folded_args, kws = resolve(new_args, kws)
 
@@ -527,9 +544,9 @@ class _OverloadFunctionTemplate(AbstractTemplate):
                 self._compiled_overloads[sig.args] = disp_type.get_overload(sig)
                 # store the inliner information, it's used later in the cost
                 # model function call
-                iinfo = _inline_info(ir, typemap, calltypes, sig)
-                self._inline_overloads[sig.args] = {'folded_args': folded_args,
-                                                    'iinfo': iinfo}
+            iinfo = _inline_info(ir, typemap, calltypes, sig)
+            self._inline_overloads[sig.args] = {'folded_args': folded_args,
+                                                'iinfo': iinfo}
         else:
             sig = disp_type.get_call_type(self.context, new_args, kws)
             self._compiled_overloads[sig.args] = disp_type.get_overload(sig)
diff --git a/numba/core/untyped_passes.py b/numba/core/untyped_passes.py
index 4daf76be3..ea83e25e2 100644
--- a/numba/core/untyped_passes.py
+++ b/numba/core/untyped_passes.py
@@ -203,6 +203,7 @@ class InlineClosureLikes(FunctionPass):
             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()
@@ -288,6 +289,16 @@ class InlineInlinables(FunctionPass):
             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.
@@ -299,13 +310,18 @@ class InlineInlinables(FunctionPass):
                     expr = instr.value
                     if isinstance(expr, ir.Expr) and expr.op == 'call':
                         if guard(self._do_work, state, work_list, block, i,
-                                 expr):
+                                 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:
@@ -314,9 +330,7 @@ class InlineInlinables(FunctionPass):
             print(''.center(80, '-'))
         return True
 
-    def _do_work(self, state, work_list, block, i, expr):
-        from numba.core.inline_closurecall import (inline_closure_call,
-                                                   callee_ir_validator)
+    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
 
@@ -373,12 +387,12 @@ class InlineInlinables(FunctionPass):
                                                     py_func_ir)
                         # if do_inline is True then inline!
                         if do_inline:
-                            inline_closure_call(
-                                state.func_ir,
-                                pyfunc.__globals__,
-                                block, i, pyfunc,
-                                work_list=work_list,
-                                callee_validator=callee_ir_validator)
+                            _, _, _, 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
 


Test patch:
diff --git a/numba/tests/test_ir_inlining.py b/numba/tests/test_ir_inlining.py
index 7977b1fe4..f30227eba 100644
--- a/numba/tests/test_ir_inlining.py
+++ b/numba/tests/test_ir_inlining.py
@@ -1067,5 +1067,65 @@ class TestInlineOptions(TestCase):
         self.assertIs(model.value, cost_model)
 
 
+class TestInlineMiscIssues(TestCase):
+
+    def test_issue4691(self):
+        def output_factory(array, dtype):
+            pass
+
+        @overload(output_factory, inline='always')
+        def ol_output_factory(array, dtype):
+            if isinstance(array, types.npytypes.Array):
+                def impl(array, dtype):
+                    shape = array.shape[3:]
+                    return np.zeros(shape, dtype=dtype)
+
+                return impl
+
+        @njit(nogil=True)
+        def fn(array):
+            out = output_factory(array, array.dtype)
+            return out
+
+        @njit(nogil=True)
+        def fn2(array):
+            return np.zeros(array.shape[3:], dtype=array.dtype)
+
+        fn(np.ones((10, 20, 30, 40, 50)))
+        fn2(np.ones((10, 20, 30, 40, 50)))
+
+    def test_issue4693(self):
+
+        @njit(inline='always')
+        def inlining(array):
+            if array.ndim != 1:
+                raise ValueError("Invalid number of dimensions")
+
+            return array
+
+        @njit
+        def fn(array):
+            return inlining(array)
+
+        fn(np.zeros(10))
+
+    def test_issue5476(self):
+        # Actual issue has the ValueError passed as an arg to `inlining` so is
+        # a constant inference error
+        @njit(inline='always')
+        def inlining():
+            msg = 'Something happened'
+            raise ValueError(msg)
+
+        @njit
+        def fn():
+            return inlining()
+
+        with self.assertRaises(ValueError) as raises:
+            fn()
+
+        self.assertIn("Something happened", str(raises.exception))
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/numba/tests/test_mixed_tuple_unroller.py b/numba/tests/test_mixed_tuple_unroller.py
index 412930a49..902fb0ac7 100644
--- a/numba/tests/test_mixed_tuple_unroller.py
+++ b/numba/tests/test_mixed_tuple_unroller.py
@@ -1583,6 +1583,7 @@ class TestMore(TestCase):
         # literal_unroll
         bar()
 
+    @unittest.skip("inlining of foo doesn't have const prop so y isn't const")
     def test_inlined_unroll_list(self):
         @njit(inline='always')
         def foo(y):