Repository: numba/numba

Issue:
Providing many keyword arguments triggers assertion error with Python 3.10 due to unsupported bytecode
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

-->

It seems that Python 3.10 produces a different bytecode instruction, `CALL_FUNCTION_EX` when using a large number of keyword arguments. From my testing it seems like this occurs once you reach 16 keyword arguments. I only see this behavior on Python 3.10 and not Python 3.9. Here is a reproducer:

```python
import numba

@numba.njit
def my_func1(a=None, b=None, c=None, d=None, e=None, f=None, g=None, h=None, i=None, j=None, k=None, l=None, m=None, n=None, o=None, p=None):
    return my_func2(
        a=a,
        b=b,
        c=c,
        d=d,
        e=e,
        f=f,
        g=g,
        h=h,
        i=i,
        j=j,
        k=k,
        l=l,
        m=m,
        n=n,
        o=o,
        p=p
    )


@numba.njit
def my_func2(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p):
    return


print(my_func1())
```


Discussion:
It seems like this example does produce issues with Python 3.9, but its a different issue that says `No type info available for none as a callable.`
I found the mistake in my reproducer and edited the issue. I don't see any issue with Python 3.9 but I do with Python 3.10.
I'm not sure if this should be classified as a bug, but I believe this is a regression due to changes in the bytecode.
Here is the full bytecode in case this is helpful:

```python3
>          0	NOP(arg=None, lineno=5)
           2	LOAD_GLOBAL(arg=0, lineno=5)
           4	LOAD_CONST(arg=17, lineno=5)
           6	BUILD_MAP(arg=0, lineno=5)
           8	LOAD_CONST(arg=1, lineno=5)
          10	LOAD_FAST(arg=0, lineno=6)
          12	MAP_ADD(arg=1, lineno=6)
          14	LOAD_CONST(arg=2, lineno=5)
          16	LOAD_FAST(arg=1, lineno=7)
          18	MAP_ADD(arg=1, lineno=7)
          20	LOAD_CONST(arg=3, lineno=5)
          22	LOAD_FAST(arg=2, lineno=8)
          24	MAP_ADD(arg=1, lineno=8)
          26	LOAD_CONST(arg=4, lineno=5)
          28	LOAD_FAST(arg=3, lineno=9)
          30	MAP_ADD(arg=1, lineno=9)
          32	LOAD_CONST(arg=5, lineno=5)
          34	LOAD_FAST(arg=4, lineno=10)
          36	MAP_ADD(arg=1, lineno=10)
          38	LOAD_CONST(arg=6, lineno=5)
          40	LOAD_FAST(arg=5, lineno=11)
          42	MAP_ADD(arg=1, lineno=11)
          44	LOAD_CONST(arg=7, lineno=5)
          46	LOAD_FAST(arg=6, lineno=12)
          48	MAP_ADD(arg=1, lineno=12)
          50	LOAD_CONST(arg=8, lineno=5)
          52	LOAD_FAST(arg=7, lineno=13)
          54	MAP_ADD(arg=1, lineno=13)
          56	LOAD_CONST(arg=9, lineno=5)
          58	LOAD_FAST(arg=8, lineno=14)
          60	MAP_ADD(arg=1, lineno=14)
          62	LOAD_CONST(arg=10, lineno=5)
          64	LOAD_FAST(arg=9, lineno=15)
          66	MAP_ADD(arg=1, lineno=15)
          68	LOAD_CONST(arg=11, lineno=5)
          70	LOAD_FAST(arg=10, lineno=16)
          72	MAP_ADD(arg=1, lineno=16)
          74	LOAD_CONST(arg=12, lineno=5)
          76	LOAD_FAST(arg=11, lineno=17)
          78	MAP_ADD(arg=1, lineno=17)
          80	LOAD_CONST(arg=13, lineno=5)
          82	LOAD_FAST(arg=12, lineno=18)
          84	MAP_ADD(arg=1, lineno=18)
          86	LOAD_CONST(arg=14, lineno=5)
          88	LOAD_FAST(arg=13, lineno=19)
          90	MAP_ADD(arg=1, lineno=19)
          92	LOAD_CONST(arg=15, lineno=5)
          94	LOAD_FAST(arg=14, lineno=20)
          96	MAP_ADD(arg=1, lineno=20)
          98	LOAD_CONST(arg=16, lineno=5)
         100	LOAD_FAST(arg=15, lineno=21)
         102	MAP_ADD(arg=1, lineno=21)
         104	CALL_FUNCTION_EX(arg=1, lineno=5)
         106	RETURN_VALUE(arg=None, lineno=5)
```
@njriasan thank you very much for reporting this! I concur that this is probably a 3.10 issue. It probably slipped through because we hadn't had a unit test for this. For reference the the backtrace is:

```
 💣 zsh» python issue7812.py
Traceback (most recent call last):
  File "/Users/esc/git/numba/issue7812.py", line 30, in <module>
    print(my_func1())
  File "/Users/esc/git/numba/numba/core/dispatcher.py", line 471, in _compile_for_args
    error_rewrite(e, 'unsupported_error')
  File "/Users/esc/git/numba/numba/core/dispatcher.py", line 407, in error_rewrite
    raise e
  File "/Users/esc/git/numba/numba/core/dispatcher.py", line 420, in _compile_for_args
    return_val = self.compile(tuple(argtypes))
  File "/Users/esc/git/numba/numba/core/dispatcher.py", line 965, in compile
    cres = self._compiler.compile(args, return_type)
  File "/Users/esc/git/numba/numba/core/dispatcher.py", line 125, in compile
    status, retval = self._compile_cached(args, return_type)
  File "/Users/esc/git/numba/numba/core/dispatcher.py", line 139, in _compile_cached
    retval = self._compile_core(args, return_type)
  File "/Users/esc/git/numba/numba/core/dispatcher.py", line 152, in _compile_core
    cres = compiler.compile_extra(self.targetdescr.typing_context,
  File "/Users/esc/git/numba/numba/core/compiler.py", line 707, in compile_extra
    return pipeline.compile_extra(func)
  File "/Users/esc/git/numba/numba/core/compiler.py", line 443, in compile_extra
    return self._compile_bytecode()
  File "/Users/esc/git/numba/numba/core/compiler.py", line 511, in _compile_bytecode
    return self._compile_core()
  File "/Users/esc/git/numba/numba/core/compiler.py", line 490, in _compile_core
    raise e
  File "/Users/esc/git/numba/numba/core/compiler.py", line 477, in _compile_core
    pm.run(self.state)
  File "/Users/esc/git/numba/numba/core/compiler_machinery.py", line 353, in run
    raise patched_exception
  File "/Users/esc/git/numba/numba/core/compiler_machinery.py", line 341, in run
    self._runPass(idx, pass_inst, state)
  File "/Users/esc/git/numba/numba/core/compiler_lock.py", line 35, in _acquire_compile_lock
    return func(*args, **kwargs)
  File "/Users/esc/git/numba/numba/core/compiler_machinery.py", line 296, in _runPass
    mutated |= check(pss.run_pass, internal_state)
  File "/Users/esc/git/numba/numba/core/compiler_machinery.py", line 269, in check
    mangled = func(compiler_state)
  File "/Users/esc/git/numba/numba/core/untyped_passes.py", line 86, in run_pass
    func_ir = interp.interpret(bc)
  File "/Users/esc/git/numba/numba/core/interpreter.py", line 358, in interpret
    flow.run()
  File "/Users/esc/git/numba/numba/core/byteflow.py", line 107, in run
    runner.dispatch(state)
  File "/Users/esc/git/numba/numba/core/byteflow.py", line 282, in dispatch
    fn(state, inst)
  File "/Users/esc/git/numba/numba/core/byteflow.py", line 832, in op_CALL_FUNCTION_EX
    raise UnsupportedError(errmsg)
numba.core.errors.UnsupportedError: Failed in nopython mode pipeline (step: analyzing bytecode)
CALL_FUNCTION_EX with **kwargs not supported

```
We will have to decide next week if this should be a bug or a feature!
I'm reasonably sure that this can be fixed, but it potentially involves a lot of work. Maybe the fix could come as rewrite of the sequence or through the use of `LiteralStrKeyDict`, the latter would likely permit more use of `**kwargs` in general.
i'd guess rewriting the sequence will be way faster (dev and runtime)
> We will have to decide next week if this should be a bug or a feature!

I would argue this should be considered a bug because its a regression of behavior that was supported in Python 3.9.
> i'd guess rewriting the sequence will be way faster (dev and runtime)

@sklam could you elaborate on what you and @stuartarchibald are referring to when you say rewriting the sequence? If possible could you point me to where I can look in the source code to understand this better?
@njriasan, we are referring to rewriting the bytecode sequence as part of [`interpreter.py`](https://github.com/numba/numba/blob/main/numba/core/interpreter.py). An example of the rewrite can be find at:

https://github.com/numba/numba/blob/d9efc9ebce26c61eee2b3665144d0e321327131e/numba/core/interpreter.py#L82
I've found the section of CPython responsible for this change.

https://github.com/python/cpython/blob/a58ebcc701dd6c43630df941481475ff0f615a81/Python/compile.c#L55
https://github.com/python/cpython/blob/a58ebcc701dd6c43630df941481475ff0f615a81/Python/compile.c#L4442

It does occur only when you have > 16 keyword arguments.
For clarity purposes, here is the Python3.9 branch. Python3.10 just added that extra case.

https://github.com/python/cpython/blob/c11f41b2c989121e34ccf94cb5b7896fbdb859ab/Python/compile.c#L4265
> I've found the section of CPython responsible for this change.
> 
> https://github.com/python/cpython/blob/a58ebcc701dd6c43630df941481475ff0f615a81/Python/compile.c#L55 https://github.com/python/cpython/blob/a58ebcc701dd6c43630df941481475ff0f615a81/Python/compile.c#L4442
> 
> It does occur only when you have > 16 keyword arguments.

Correction, this occurs when (n_args / 2) + n_kws > 15
@njriasan awesome, great find! 🕵️‍♂️
I'm going to use this issue to record some background information on this issue and the optimizations made. I'll include all of this information in the code as well, but since it may take a bit of time to construct a draft PR, I want to get this information written down while it is fresh.

I did a deeper dive into the bytecode and it seems like there are 6 "distinct" cases where the bytecode differs and this path is taken. For all of these case I assume `(n_args / 2) + n_kws > 15` holds. The cases are:

1. `No args and > 15 KWS`
2. `< 30 args and < 15 KWS`
3. `> 30 args and <= 15 KWS`
4. `<= 30 args and > 15 KWS`
5. `> 30 args and > 15 KWS`

The main cases to consider are when `(n_args / 2) > 15` or `n_kws > 15` holds independently vs when only `(n_args / 2) + n_kws > 15` holds. I'll briefly discuss the bytecode patterns independently for args and kws. I should note that  `> 30 args and No KWS` is already handled by existing optimizations and thus omitted.

## > 15 KWS

This is the case from the original example. The bytecode above shows the example, but the general structure is

```python
BUILD_MAP # Construct the map
# START REPEAT N times, once for each kwarg
LOAD_CONST # Load a constant for the name of the argument
LOAD_FAST # Load the argument
MAP_ADD # Append the (key, value) pair to the map
# END REPEAT N times, once for each kwarg
CALL_FUNCTION_EX # Call the function with the map
```

## <= 15 KWS

When there are fewer than 15 KWS, the bytecode is somewhat simpler because the map is built on the stack. Instead the bytecode is:

```python
# START REPEAT N times, once for each kwarg
LOAD_FAST # Load the argument
# END REPEAT N times, once for each kwarg
LOAD_CONST # Load the size of the map
BUILD_CONST_KEY_MAP # Build the map
```
## > 30 ARGS

When there are > 30 args, the input arguments get converted to a tuple after a list is already created.

```python
BUILD_LIST # Create the list
# START REPEAT N times, once for each arg
LOAD_FAST # Load the argument
LIST_APPEND # Add the argument to the list
# END REPEAT N times, once for each arg
LIST_TO_TUPLE # Convert the args to a tuple.
```
I believe this should be similar to the existing `peep_hole_list_to_tuple`, but it cannot just be reused as is because it won't contain `extend`.

## <= 30 ARGS

When there are <= 30 args, the input args are just a tuple.

```python
# START REPEAT N times, once for each arg
LOAD_FAST # Load the argument
# END REPEAT N times, once for each arg
BUILD_TUPLE # Create the tuple.
```
To clarify, the `LOAD_FAST` is just the way to load the argument in my example. It may be many other things in general.

Resolving patch:
diff --git a/numba/core/byteflow.py b/numba/core/byteflow.py
--- a/numba/core/byteflow.py
+++ b/numba/core/byteflow.py
@@ -827,13 +827,17 @@ def op_CALL_FUNCTION_KW(self, state, inst):
         state.push(res)
 
     def op_CALL_FUNCTION_EX(self, state, inst):
-        if inst.arg & 1:
+        if inst.arg & 1 and PYVERSION != (3, 10):
             errmsg = "CALL_FUNCTION_EX with **kwargs not supported"
             raise UnsupportedError(errmsg)
+        if inst.arg & 1:
+            varkwarg = state.pop()
+        else:
+            varkwarg = None
         vararg = state.pop()
         func = state.pop()
         res = state.make_temp()
-        state.append(inst, func=func, vararg=vararg, res=res)
+        state.append(inst, func=func, vararg=vararg, varkwarg=varkwarg, res=res)
         state.push(res)
 
     def _dup_topx(self, state, inst, count):
diff --git a/numba/core/interpreter.py b/numba/core/interpreter.py
--- a/numba/core/interpreter.py
+++ b/numba/core/interpreter.py
@@ -3,9 +3,10 @@
 import dis
 import operator
 import logging
+import textwrap
 
 from numba.core import errors, dataflow, controlflow, ir, config
-from numba.core.errors import NotDefinedError, error_extras
+from numba.core.errors import NotDefinedError, UnsupportedError, error_extras
 from numba.core.utils import (PYVERSION, BINOPS_TO_OPERATORS,
                               INPLACE_BINOPS_TO_OPERATORS,)
 from numba.core.byteflow import Flow, AdaptDFA, AdaptCFA
@@ -79,6 +80,646 @@ def get_assignment_source(self, destname):
         return None
 
 
+def _remove_assignment_definition(old_body, idx, func_ir):
+    """
+    Deletes the definition defined for old_body at index idx
+    from func_ir. We assume this stmt will be deleted from
+    new_body.
+    """
+    lhs = old_body[idx].target.name
+    rhs = old_body[idx].value
+    if rhs in func_ir._definitions[lhs]:
+        func_ir._definitions[lhs].remove(rhs)
+    elif len(func_ir._definitions[lhs]) == 1:
+        # There appears to be an issue where the RHS of the
+        # definitions doesn't match after the list -> tuple
+        # conversion.
+        # For example the statement is:
+        #
+        #   172list_to_tuple.116 = $170list_append.115
+        #
+        # but the definition is:
+        #
+        #   list_to_tuple(info=('$4build_list.1',))
+        #
+        func_ir._definitions[lhs].clear()
+    else:
+        raise UnsupportedError(
+            "Inconsistency found in the definitions while executing"
+            " peep_hole_call_function_ex_to_call_function_kw. This suggests"
+            " an internal error or inconsistency elsewhere in the compiler."
+        )
+
+
+def _call_function_ex_replace_kws_small(
+    old_body, keyword_expr, new_body, buildmap_idx, func_ir
+):
+    """
+    Extracts the kws args passed as varkwarg
+    for CALL_FUNCTION_EX. This pass is taken when
+    n_kws <= 15 and the bytecode looks like:
+
+        # Start for each argument
+        LOAD_FAST  # Load each argument.
+        # End for each argument
+        ...
+        BUILD_CONST_KEY_MAP # Build a map
+
+    In the generated IR, the varkwarg refers
+    to a single build_map that contains all of the
+    kws. In addition to returning the kws, this
+    function updates new_body to remove all usage
+    of the map.
+    """
+    kws = keyword_expr.items.copy()
+    # kws are required to have constant keys.
+    # We update these with the value_indexes
+    value_indexes = keyword_expr.value_indexes
+    for key, index in value_indexes.items():
+        kws[index] = (key, kws[index][1])
+    # Remove the build_map by setting the list
+    # index to None. Nones will be removed later.
+    new_body[buildmap_idx] = None
+    # Remove the definition.
+    _remove_assignment_definition(old_body, buildmap_idx, func_ir)
+    return kws
+
+
+def _call_function_ex_replace_kws_large(
+    old_body, buildmap_name, buildmap_idx, search_end, new_body, func_ir, errmsg
+):
+    """
+    Extracts the kws args passed as varkwarg
+    for CALL_FUNCTION_EX. This pass is taken when
+    n_kws > 15 and the bytecode looks like:
+
+        BUILD_MAP # Construct the map
+        # Start for each argument
+        LOAD_CONST # Load a constant for the name of the argument
+        LOAD_FAST  # Load each argument.
+        MAP_ADD # Append the (key, value) pair to the map
+        # End for each argument
+
+    In the IR generated, the initial build map is empty and a series
+    of setitems are applied afterwards. THE IR looks like:
+
+        $build_map_var = build_map(items=[])
+        $constvar = const(str, ...) # create the const key
+        # CREATE THE ARGUMENT, This may take multiple lines.
+        $created_arg = ...
+        $var = getattr(
+            value=$build_map_var,
+            attr=__setitem__,
+        )
+        $unused_var = call $var($constvar, $created_arg)
+
+    We iterate through the IR, deleting all usages of the buildmap
+    from the new_body, and adds the kws to a new kws list.
+    """
+    # Remove the build_map from the body.
+    new_body[buildmap_idx] = None
+    # Remove the definition.
+    _remove_assignment_definition(old_body, buildmap_idx, func_ir)
+    kws = []
+    search_start = buildmap_idx + 1
+    while search_start <= search_end:
+        # The first value must be a constant.
+        const_stmt = old_body[search_start]
+        if not (
+            isinstance(const_stmt, ir.Assign)
+            and isinstance(const_stmt.value, ir.Const)
+        ):
+            # We cannot handle this format so raise the
+            # original error message.
+            raise UnsupportedError(errmsg)
+        key_var_name = const_stmt.target.name
+        key_val = const_stmt.value.value
+        search_start += 1
+        # Now we need to search for a getattr with setitem
+        found_getattr = False
+        while (
+            search_start <= search_end
+            and not found_getattr
+        ):
+            getattr_stmt = old_body[search_start]
+            if (
+                isinstance(getattr_stmt, ir.Assign)
+                and isinstance(getattr_stmt.value, ir.Expr)
+                and getattr_stmt.value.op == "getattr"
+                and (
+                    getattr_stmt.value.value.name
+                    == buildmap_name
+                )
+                and getattr_stmt.value.attr == "__setitem__"
+            ):
+                found_getattr = True
+            else:
+                # If the argument is "created" in JIT, then there
+                # will be intermediate operations in between setitems.
+                # For example we have arg5=pow(arg5, 2),
+                # then the IR would look like:
+                #
+                #   # Creation of the constant key.
+                #   $const44.26 = const(str, arg5)
+                #
+                #   # Argument creation. This is the section we are skipping
+                #   $46load_global.27 = global(pow: <built-in function pow>)
+                #   $const50.29 = const(int, 2)
+                #   $call.30 = call $46load_global.27(arg5, $const50.29)
+                #
+                #   # Setitem with arg5
+                #   $54map_add.31 = getattr(value=$map.2, attr=__setitem__)
+                #   $54map_add.32 = call $54map_add.31($const44.26, $call.30)
+                search_start += 1
+        if (
+            not found_getattr
+            or search_start == search_end
+        ):
+            # We cannot handle this format so raise the
+            # original error message.
+            raise UnsupportedError(errmsg)
+        setitem_stmt = old_body[search_start + 1]
+        if not (
+            isinstance(setitem_stmt, ir.Assign)
+            and isinstance(setitem_stmt.value, ir.Expr)
+            and setitem_stmt.value.op == "call"
+            and (
+                setitem_stmt.value.func.name
+                == getattr_stmt.target.name
+            )
+            and len(setitem_stmt.value.args) == 2
+            and (
+                setitem_stmt.value.args[0].name
+                == key_var_name
+            )
+        ):
+            # A call statement should always immediately follow the
+            # getattr. If for some reason this doesn't match the code
+            # format, we raise the original error message. This check
+            # is meant as a precaution.
+            raise UnsupportedError(errmsg)
+        arg_var = setitem_stmt.value.args[1]
+        # Append the (key, value) pair.
+        kws.append((key_val, arg_var))
+        # Remove the __setitem__ getattr and call
+        new_body[search_start] = None
+        new_body[search_start + 1] = None
+        # Remove the definitions.
+        _remove_assignment_definition(old_body, search_start, func_ir)
+        _remove_assignment_definition(old_body, search_start + 1, func_ir)
+        search_start += 2
+    return kws
+
+
+def _call_function_ex_replace_args_small(
+    old_body, tuple_expr, new_body, buildtuple_idx, func_ir
+):
+    """
+    Extracts the args passed as vararg
+    for CALL_FUNCTION_EX. This pass is taken when
+    n_args <= 30 and the bytecode looks like:
+
+        # Start for each argument
+        LOAD_FAST  # Load each argument.
+        # End for each argument
+        ...
+        BUILD_TUPLE # Create a tuple of the arguments
+
+    In the IR generated, the vararg refer
+    to a single build_tuple that contains all of the
+    args. In addition to returning the args, this
+    function updates new_body to remove all usage
+    of the tuple.
+    """
+    # Delete the build tuple
+    new_body[buildtuple_idx] = None
+    # Remove the definition.
+    _remove_assignment_definition(old_body, buildtuple_idx, func_ir)
+    # Return the args.
+    return tuple_expr.items
+
+
+def _call_function_ex_replace_args_large(
+    old_body, vararg_stmt, new_body, search_end, func_ir, errmsg
+):
+    """
+    Extracts the args passed as vararg
+    for CALL_FUNCTION_EX. This pass is taken when
+    n_args > 30 and the bytecode looks like:
+
+        BUILD_TUPLE # Create a list to append to
+        # Start for each argument
+        LOAD_FAST  # Load each argument.
+        LIST_APPEND # Add the argument to the list
+        # End for each argument
+        ...
+        LIST_TO_TUPLE # Convert the args to a tuple.
+
+    In the IR generated, the tuple is created by concatenating
+    together several 1 element tuples to an initial empty tuple.
+    We traverse backwards in the IR, collecting args, until we
+    find the original empty tuple. For example, the IR might
+    look like:
+
+        $orig_tuple = build_tuple(items=[])
+        $first_var = build_tuple(items=[Var(arg0, test.py:6)])
+        $next_tuple = $orig_tuple + $first_var
+        ...
+        $final_var = build_tuple(items=[Var(argn, test.py:6)])
+        $final_tuple = $prev_tuple + $final_var
+        $varargs_var = $final_tuple
+    """
+    # We traverse to the front of the block to look for the original
+    # tuple.
+    search_start = 0
+    total_args = []
+    if (
+        isinstance(vararg_stmt, ir.Assign)
+        and isinstance(vararg_stmt.value, ir.Var)
+    ):
+        target_name = vararg_stmt.value.name
+        # If there is an initial assignment, delete it
+        new_body[search_end] = None
+        # Remove the definition.
+        _remove_assignment_definition(old_body, search_end, func_ir)
+        search_end -= 1
+    else:
+        # There must always be an initial assignement
+        # https://github.com/numba/numba/blob/59fa2e335be68148b3bd72a29de3ff011430038d/numba/core/interpreter.py#L259-L260
+        # If this changes we may need to support this branch.
+        raise AssertionError("unreachable")
+    # Traverse backwards to find all concatentations
+    # until eventually reaching the original empty tuple.
+    while search_end >= search_start:
+        concat_stmt = old_body[search_end]
+        if (
+            isinstance(concat_stmt, ir.Assign)
+            and concat_stmt.target.name == target_name
+            and isinstance(concat_stmt.value, ir.Expr)
+            and concat_stmt.value.op == "build_tuple"
+            and not concat_stmt.value.items
+        ):
+            new_body[search_end] = None
+            # Remove the definition.
+            _remove_assignment_definition(old_body, search_end, func_ir)
+            # If we have reached the build_tuple we exit.
+            break
+        else:
+            # We expect to find another arg to append.
+            # The first stmt must be a binop "add"
+            if (search_end == search_start) or not (
+                isinstance(concat_stmt, ir.Assign)
+                and (
+                    concat_stmt.target.name
+                    == target_name
+                )
+                and isinstance(
+                    concat_stmt.value, ir.Expr
+                )
+                and concat_stmt.value.op == "binop"
+                and concat_stmt.value.fn == operator.add
+            ):
+                # We cannot handle this format.
+                raise UnsupportedError(errmsg)
+            lhs_name = concat_stmt.value.lhs.name
+            rhs_name = concat_stmt.value.rhs.name
+            # The previous statment should be a
+            # build_tuple containing the arg.
+            arg_tuple_stmt = old_body[search_end - 1]
+            if not (
+                isinstance(arg_tuple_stmt, ir.Assign)
+                and isinstance(
+                    arg_tuple_stmt.value, ir.Expr
+                )
+                and (
+                    arg_tuple_stmt.value.op
+                    == "build_tuple"
+                )
+                and len(arg_tuple_stmt.value.items) == 1
+            ):
+                # We cannot handle this format.
+                raise UnsupportedError(errmsg)
+            if arg_tuple_stmt.target.name == lhs_name:
+                # The tuple should always be generated on the RHS.
+                raise AssertionError("unreachable")
+            elif arg_tuple_stmt.target.name == rhs_name:
+                target_name = lhs_name
+            else:
+                # We cannot handle this format.
+                raise UnsupportedError(errmsg)
+            total_args.append(
+                arg_tuple_stmt.value.items[0]
+            )
+            new_body[search_end] = None
+            new_body[search_end - 1] = None
+            # Remove the definitions.
+            _remove_assignment_definition(old_body, search_end, func_ir)
+            _remove_assignment_definition(old_body, search_end - 1, func_ir)
+            search_end -= 2
+            # Avoid any space between appends
+            keep_looking = True
+            while search_end >= search_start and keep_looking:
+                next_stmt = old_body[search_end]
+                if (
+                    isinstance(next_stmt, ir.Assign)
+                    and (
+                        next_stmt.target.name
+                        == target_name
+                    )
+                ):
+                    keep_looking = False
+                else:
+                    # If the argument is "created" in JIT, then there
+                    # will be intermediate operations in between appends.
+                    # For example if the next arg after arg4 is pow(arg5, 2),
+                    # then the IR would look like:
+                    #
+                    #   # Appending arg4
+                    #   $arg4_tup = build_tuple(items=[arg4])
+                    #   $append_var.5 = $append_var.4 + $arg4_tup
+                    #
+                    #   # Creation of arg5.
+                    #   # This is the section that we are skipping.
+                    #   $32load_global.20 = global(pow: <built-in function pow>)
+                    #   $const36.22 = const(int, 2)
+                    #   $call.23 = call $32load_global.20(arg5, $const36.22)
+                    #
+                    #   # Appending arg5
+                    #   $arg5_tup = build_tuple(items=[$call.23])
+                    #   $append_var.6 = $append_var.5 + $arg5_tup
+                    search_end -= 1
+    if search_end == search_start:
+        # If we reached the start we never found the build_tuple.
+        # We cannot handle this format so raise the
+        # original error message.
+        raise UnsupportedError(errmsg)
+    # Reverse the arguments so we get the correct order.
+    return total_args[::-1]
+
+
+def peep_hole_call_function_ex_to_call_function_kw(func_ir):
+    """
+    This peephole rewrites a bytecode sequence unique to Python 3.10
+    where CALL_FUNCTION_EX is used instead of CALL_FUNCTION_KW because of
+    stack limitations set by CPython. This limitation is imposed whenever
+    a function call has too many arguments or keyword arguments.
+
+    https://github.com/python/cpython/blob/a58ebcc701dd6c43630df941481475ff0f615a81/Python/compile.c#L55
+    https://github.com/python/cpython/blob/a58ebcc701dd6c43630df941481475ff0f615a81/Python/compile.c#L4442
+
+    In particular, this change is imposed whenever (n_args / 2) + n_kws > 15.
+
+    Different bytecode is generated for args depending on if n_args > 30
+    or n_args <= 30 and similarly if n_kws > 15 or n_kws <= 15.
+
+    This function unwraps the *args and **kwargs in the function call
+    and places these values directly into the args and kwargs of the call.
+    """
+    # All changes are local to the a single block
+    # so it can be traversed in any order.
+    errmsg = textwrap.dedent("""
+        CALL_FUNCTION_EX with **kwargs not supported.
+        If you are not using **kwargs this may indicate that
+        you have a large number of kwargs and are using inlined control
+        flow. You can resolve this issue by moving the control flow out of
+        the function call. For example, if you have
+
+            f(a=1 if flag else 0, ...)
+
+        Replace that with:
+
+            a_val = 1 if flag else 0
+            f(a=a_val, ...)""")
+    for blk in func_ir.blocks.values():
+        blk_changed = False
+        new_body = []
+        for i, stmt in enumerate(blk.body):
+            if (
+                isinstance(stmt, ir.Assign)
+                and isinstance(stmt.value, ir.Expr)
+                and stmt.value.op == "call"
+                and stmt.value.varkwarg is not None
+            ):
+                blk_changed = True
+                call = stmt.value
+                args = call.args
+                kws = call.kws
+                # We need to check the call expression contents if
+                # it contains either vararg or varkwarg. If it contains
+                # varkwarg we need to update the IR. If it just contains
+                # vararg we don't need to update the IR, but we need to
+                # check if peep_hole_list_to_tuple failed to replace the
+                # vararg list with a tuple. If so, we output an error
+                # message with suggested code changes.
+                vararg = call.vararg
+                varkwarg = call.varkwarg
+                start_search = i - 1
+                # varkwarg should be defined second so we start there.
+                varkwarg_loc = start_search
+                keyword_def = None
+                found = False
+                while varkwarg_loc >= 0 and not found:
+                    keyword_def = blk.body[varkwarg_loc]
+                    if (
+                        isinstance(keyword_def, ir.Assign)
+                        and keyword_def.target.name == varkwarg.name
+                    ):
+                        found = True
+                    else:
+                        varkwarg_loc -= 1
+                if (
+                    kws
+                    or not found
+                    or not (
+                        isinstance(keyword_def.value, ir.Expr)
+                        and keyword_def.value.op == "build_map"
+                    )
+                ):
+                    # If we couldn't find where the kwargs are created
+                    # then it should be a normal **kwargs call
+                    # so we produce an unsupported message.
+                    raise UnsupportedError(errmsg)
+                # Determine the kws
+                if keyword_def.value.items:
+                    # n_kws <= 15 case.
+                    # Here the IR looks like a series of
+                    # constants, then the arguments and finally
+                    # a build_map that contains all of the pairs.
+                    # For Example:
+                    #
+                    #   $const_n = const("arg_name")
+                    #   $arg_n = ...
+                    #   $kwargs_var = build_map(items=[
+                    #              ($const_0, $arg_0),
+                    #              ...,
+                    #              ($const_n, $arg_n),])
+                    kws = _call_function_ex_replace_kws_small(
+                        blk.body,
+                        keyword_def.value,
+                        new_body,
+                        varkwarg_loc,
+                        func_ir,
+                    )
+                else:
+                    # n_kws > 15 case.
+                    # Here the IR is an initial empty build_map
+                    # followed by a series of setitems with a constant
+                    # key and then the argument.
+                    # For example:
+                    #
+                    #   $kwargs_var = build_map(items=[])
+                    #   $const_0 = const("arg_name")
+                    #   $arg_0 = ...
+                    #   $my_attr = getattr(const_0, attr=__setitem__)
+                    #   $unused_var = call $my_attr($const_0, $arg_0)
+                    #   ...
+                    kws = _call_function_ex_replace_kws_large(
+                        blk.body,
+                        varkwarg.name,
+                        varkwarg_loc,
+                        i - 1,
+                        new_body,
+                        func_ir,
+                        errmsg,
+                    )
+                start_search = varkwarg_loc
+                # Vararg isn't required to be provided.
+                if vararg is not None:
+                    if args:
+                        # If we have vararg then args is expected to
+                        # be an empty list.
+                        raise UnsupportedError(errmsg)
+                    vararg_loc = start_search
+                    args_def = None
+                    found = False
+                    while vararg_loc >= 0 and not found:
+                        args_def = blk.body[vararg_loc]
+                        if (
+                            isinstance(args_def, ir.Assign)
+                            and args_def.target.name == vararg.name
+                        ):
+                            found = True
+                        else:
+                            vararg_loc -= 1
+                    if not found:
+                        # If we couldn't find where the args are created
+                        # then we can't handle this format.
+                        raise UnsupportedError(errmsg)
+                    if (
+                        isinstance(args_def.value, ir.Expr)
+                        and args_def.value.op == "build_tuple"
+                    ):
+                        # n_args <= 30 case.
+                        # Here the IR is a simple build_tuple containing
+                        # all of the args.
+                        # For example:
+                        #
+                        #  $arg_n = ...
+                        #  $varargs = build_tuple(
+                        #   items=[$arg_0, ..., $arg_n]
+                        #  )
+                        args = _call_function_ex_replace_args_small(
+                            blk.body,
+                            args_def.value,
+                            new_body,
+                            vararg_loc,
+                            func_ir,
+                        )
+                    elif (
+                        isinstance(args_def.value, ir.Expr)
+                        and args_def.value.op == "list_to_tuple"
+                    ):
+                        # If there is a call with vararg we need to check
+                        # if the list -> tuple conversion failed and if so
+                        # throw an error.
+                        raise UnsupportedError(errmsg)
+                    else:
+                        # Here the IR is an initial empty build_tuple.
+                        # Then for each arg, a new tuple with a single
+                        # element is created and one by one these are
+                        # added to a growing tuple.
+                        # For example:
+                        #
+                        #  $combo_tup_0 = build_tuple(items=[])
+                        #  $arg0 = ...
+                        #  $arg0_tup = build_tuple(items=[$arg0])
+                        #  $combo_tup_1 = $combo_tup_0 + $arg0_tup
+                        #  $arg1 = ...
+                        #  $arg1_tup = build_tuple(items=[$arg1])
+                        #  $combo_tup_2 = $combo_tup_1 + $arg1_tup
+                        #  ...
+                        #  $combo_tup_n = $combo_tup_{n-1} + $argn_tup
+                        #
+                        # In addition, the IR contains a final
+                        # assignment for the varargs that looks like:
+                        #
+                        #  $varargs_var = $combo_tup_n
+                        #
+                        # Here args_def is expected to be a simple assignment.
+                        args = _call_function_ex_replace_args_large(
+                            blk.body,
+                            args_def,
+                            new_body,
+                            vararg_loc,
+                            func_ir,
+                            errmsg,
+                        )
+                # Create a new call updating the args and kws
+                new_call = ir.Expr.call(
+                    call.func, args, kws, call.loc, target=call.target
+                )
+                # Drop the existing definition for this stmt.
+                _remove_assignment_definition(blk.body, i, func_ir)
+                # Update the statement
+                stmt = ir.Assign(new_call, stmt.target, stmt.loc)
+                # Update the definition
+                func_ir._definitions[stmt.target.name].append(new_call)
+            elif (
+                isinstance(stmt, ir.Assign)
+                and isinstance(stmt.value, ir.Expr)
+                and stmt.value.op == "call"
+                and stmt.value.vararg is not None
+            ):
+                # If there is a call with vararg we need to check
+                # if the list -> tuple conversion failed and if so
+                # throw an error.
+                call = stmt.value
+                args = call.args
+                vararg = call.vararg
+                if args:
+                    # If we have vararg then args is expected to
+                    # be an empty list.
+                    raise UnsupportedError(errmsg)
+                vararg_loc = i - 1
+                args_def = None
+                found = False
+                while vararg_loc >= 0 and not found:
+                    args_def = blk.body[vararg_loc]
+                    if (
+                        isinstance(args_def, ir.Assign)
+                        and args_def.target.name == vararg.name
+                    ):
+                        found = True
+                    else:
+                        vararg_loc -= 1
+                if not found:
+                    # If we couldn't find where the args are created
+                    # then we can't handle this format.
+                    raise UnsupportedError(errmsg)
+                else:
+                    # If this value is still a list to tuple raise the
+                    # exception.
+                    expr = blk.body[vararg_loc].value
+                    if isinstance(expr, ir.Expr) and expr.op == "list_to_tuple":
+                        raise UnsupportedError(errmsg)
+
+            new_body.append(stmt)
+        # Replace the block body if we changed the IR
+        if blk_changed:
+            blk.body = [x for x in new_body if x is not None]
+    return func_ir
+
+
 def peep_hole_list_to_tuple(func_ir):
     """
     This peephole rewrites a bytecode sequence new to Python 3.9 that looks
@@ -384,6 +1025,12 @@ def interpret(self, bytecode):
         if PYVERSION in [(3, 9), (3, 10)]:
             peepholes.append(peep_hole_list_to_tuple)
         peepholes.append(peep_hole_delete_with_exit)
+        if PYVERSION == (3, 10):
+            # peep_hole_call_function_ex_to_call_function_kw
+            # depends on peep_hole_list_to_tuple converting
+            # any large number of arguments from a list to a
+            # tuple.
+            peepholes.append(peep_hole_call_function_ex_to_call_function_kw)
 
         post_processed_ir = self.post_process(peepholes, func_ir)
         return post_processed_ir
@@ -1217,10 +1864,14 @@ def op_CALL_FUNCTION_KW(self, inst, func, args, names, res):
             expr = ir.Expr.call(func, posvals, keyvalues, loc=self.loc)
             self.store(expr, res)
 
-        def op_CALL_FUNCTION_EX(self, inst, func, vararg, res):
+        def op_CALL_FUNCTION_EX(self, inst, func, vararg, varkwarg, res):
             func = self.get(func)
             vararg = self.get(vararg)
-            expr = ir.Expr.call(func, [], [], loc=self.loc, vararg=vararg)
+            if varkwarg is not None:
+                varkwarg = self.get(varkwarg)
+            expr = ir.Expr.call(
+                func, [], [], loc=self.loc, vararg=vararg, varkwarg=varkwarg
+            )
             self.store(expr, res)
 
     def _build_tuple_unpack(self, inst, tuples, temps, is_assign):
diff --git a/numba/core/ir.py b/numba/core/ir.py
--- a/numba/core/ir.py
+++ b/numba/core/ir.py
@@ -418,12 +418,12 @@ def unary(cls, fn, value, loc):
         return cls(op=op, loc=loc, fn=fn, value=value)
 
     @classmethod
-    def call(cls, func, args, kws, loc, vararg=None, target=None):
+    def call(cls, func, args, kws, loc, vararg=None, varkwarg=None, target=None):
         assert isinstance(func, Var)
         assert isinstance(loc, Loc)
         op = 'call'
         return cls(op=op, loc=loc, func=func, args=args, kws=kws,
-                   vararg=vararg, target=target)
+                   vararg=vararg, varkwarg=varkwarg, target=target)
 
     @classmethod
     def build_tuple(cls, items, loc):


Test patch:
diff --git a/numba/tests/support.py b/numba/tests/support.py
--- a/numba/tests/support.py
+++ b/numba/tests/support.py
@@ -75,6 +75,11 @@
     "needs Python 3.10 or later"
 )
 
+skip_unless_py10 = unittest.skipUnless(
+    utils.PYVERSION == (3, 10),
+    "needs Python 3.10"
+)
+
 _msg = "SciPy needed for test"
 skip_unless_scipy = unittest.skipIf(scipy is None, _msg)
 
diff --git a/numba/tests/test_interpreter.py b/numba/tests/test_interpreter.py
new file mode 100644
--- /dev/null
+++ b/numba/tests/test_interpreter.py
@@ -0,0 +1,620 @@
+"""
+Test bytecode fixes provided in interpreter.py
+"""
+from numba import njit, objmode
+from numba.core.errors import UnsupportedError
+from numba.tests.support import TestCase, MemoryLeakMixin, skip_unless_py10
+
+
+@njit
+def sum_jit_func(
+    arg0=0,
+    arg1=0,
+    arg2=0,
+    arg3=0,
+    arg4=0,
+    arg5=0,
+    arg6=0,
+    arg7=0,
+    arg8=0,
+    arg9=0,
+    arg10=0,
+    arg11=0,
+    arg12=0,
+    arg13=0,
+    arg14=0,
+    arg15=0,
+    arg16=0,
+    arg17=0,
+    arg18=0,
+    arg19=0,
+    arg20=0,
+    arg21=0,
+    arg22=0,
+    arg23=0,
+    arg24=0,
+    arg25=0,
+    arg26=0,
+    arg27=0,
+    arg28=0,
+    arg29=0,
+    arg30=0,
+    arg31=0,
+    arg32=0,
+    arg33=0,
+    arg34=0,
+    arg35=0,
+    arg36=0,
+    arg37=0,
+    arg38=0,
+    arg39=0,
+    arg40=0,
+    arg41=0,
+    arg42=0,
+    arg43=0,
+    arg44=0,
+    arg45=0,
+    arg46=0,
+):
+    return (
+        arg0
+        + arg1
+        + arg2
+        + arg3
+        + arg4
+        + arg5
+        + arg6
+        + arg7
+        + arg8
+        + arg9
+        + arg10
+        + arg11
+        + arg12
+        + arg13
+        + arg14
+        + arg15
+        + arg16
+        + arg17
+        + arg18
+        + arg19
+        + arg20
+        + arg21
+        + arg22
+        + arg23
+        + arg24
+        + arg25
+        + arg26
+        + arg27
+        + arg28
+        + arg29
+        + arg30
+        + arg31
+        + arg32
+        + arg33
+        + arg34
+        + arg35
+        + arg36
+        + arg37
+        + arg38
+        + arg39
+        + arg40
+        + arg41
+        + arg42
+        + arg43
+        + arg44
+        + arg45
+        + arg46
+    )
+
+
+class TestCallFunctionExPeepHole(TestCase, MemoryLeakMixin):
+    """
+    gh #7812
+
+    Tests that check a peephole optimization for Function calls
+    in Python 3.10. The bytecode changes when
+    (n_args / 2) + n_kws > 15, which moves the arguments from
+    the stack into a tuple and dictionary.
+
+    This peephole optimization updates the IR to use the original format.
+    There are different paths when n_args > 30 and n_args <= 30 and when
+    n_kws > 15 and n_kws <= 15.
+    """
+    THRESHOLD_ARGS = 31
+    THRESHOLD_KWS = 16
+
+    def gen_func(self, n_args, n_kws):
+        """
+            Generates a function that calls sum_jit_func
+            with the desired number of args and kws.
+        """
+        param_list = [f"arg{i}" for i in range(n_args + n_kws)]
+        args_list = []
+        for i in range(n_args + n_kws):
+            # Call a function on every 5th argument to ensure
+            # we test function calls.
+            if i % 5 == 0:
+                arg_val = f"pow(arg{i}, 2)"
+            else:
+                arg_val = f"arg{i}"
+            args_list.append(arg_val)
+        total_params = ", ".join(param_list)
+        func_text = f"def impl({total_params}):\n"
+        func_text += "    return sum_jit_func(\n"
+        for i in range(n_args):
+            func_text += f"        {args_list[i]},\n"
+        for i in range(n_args, n_args + n_kws):
+            func_text += f"        {param_list[i]}={args_list[i]},\n"
+        func_text += "    )\n"
+        local_vars = {}
+        exec(func_text, {"sum_jit_func": sum_jit_func}, local_vars)
+        return local_vars["impl"]
+
+    @skip_unless_py10
+    def test_all_args(self):
+        """
+        Tests calling a function when n_args > 30 and
+        n_kws = 0. This shouldn't use the peephole, but
+        it should still succeed.
+        """
+        total_args = [i for i in range(self.THRESHOLD_ARGS)]
+        f = self.gen_func(self.THRESHOLD_ARGS, 0)
+        py_func = f
+        cfunc = njit()(f)
+        a = py_func(*total_args)
+        b = cfunc(*total_args)
+        self.assertEqual(a, b)
+
+    @skip_unless_py10
+    def test_all_kws(self):
+        """
+        Tests calling a function when n_kws > 15 and
+        n_args = 0.
+        """
+        total_args = [i for i in range(self.THRESHOLD_KWS)]
+        f = self.gen_func(0, self.THRESHOLD_KWS)
+        py_func = f
+        cfunc = njit()(f)
+        a = py_func(*total_args)
+        b = cfunc(*total_args)
+        self.assertEqual(a, b)
+
+    @skip_unless_py10
+    def test_small_args_small_kws(self):
+        """
+        Tests calling a function when (n_args / 2) + n_kws > 15,
+        but n_args <= 30 and n_kws <= 15
+        """
+        used_args = self.THRESHOLD_ARGS - 1
+        used_kws = self.THRESHOLD_KWS - 1
+        total_args = [i for i in range((used_args) + (used_kws))]
+        f = self.gen_func(used_args, used_kws)
+        py_func = f
+        cfunc = njit()(f)
+        a = py_func(*total_args)
+        b = cfunc(*total_args)
+        self.assertEqual(a, b)
+
+    @skip_unless_py10
+    def test_small_args_large_kws(self):
+        """
+        Tests calling a function when (n_args / 2) + n_kws > 15,
+        but n_args <= 30 and n_kws > 15
+        """
+        used_args = self.THRESHOLD_ARGS - 1
+        used_kws = self.THRESHOLD_KWS
+        total_args = [i for i in range((used_args) + (used_kws))]
+        f = self.gen_func(used_args, used_kws)
+        py_func = f
+        cfunc = njit()(f)
+        a = py_func(*total_args)
+        b = cfunc(*total_args)
+        self.assertEqual(a, b)
+
+    @skip_unless_py10
+    def test_large_args_small_kws(self):
+        """
+        Tests calling a function when (n_args / 2) + n_kws > 15,
+        but n_args > 30 and n_kws <= 15
+        """
+        used_args = self.THRESHOLD_ARGS
+        used_kws = self.THRESHOLD_KWS - 1
+        total_args = [i for i in range((used_args) + (used_kws))]
+        f = self.gen_func(used_args, used_kws)
+        py_func = f
+        cfunc = njit()(f)
+        a = py_func(*total_args)
+        b = cfunc(*total_args)
+        self.assertEqual(a, b)
+
+    @skip_unless_py10
+    def test_large_args_large_kws(self):
+        """
+        Tests calling a function when (n_args / 2) + n_kws > 15,
+        but n_args > 30 and n_kws > 15
+        """
+        used_args = self.THRESHOLD_ARGS
+        used_kws = self.THRESHOLD_KWS
+        total_args = [i for i in range((used_args) + (used_kws))]
+        f = self.gen_func(used_args, used_kws)
+        py_func = f
+        cfunc = njit()(f)
+        a = py_func(*total_args)
+        b = cfunc(*total_args)
+        self.assertEqual(a, b)
+
+    @skip_unless_py10
+    def test_large_kws_objmode(self):
+        """
+        Tests calling an objectmode function with > 15 return values.
+        """
+        def py_func():
+            return (
+                0,
+                1,
+                2,
+                3,
+                4,
+                5,
+                6,
+                7,
+                8,
+                9,
+                10,
+                11,
+                12,
+                13,
+                14,
+                15,
+            )
+
+        @njit
+        def objmode_func():
+            """
+            Wrapper to call py_func from objmode. This tests
+            large kws with objmode. If the definition for the
+            call is not properly updated this test will fail.
+            """
+            with objmode(
+                a='int64',
+                b='int64',
+                c='int64',
+                d='int64',
+                e='int64',
+                f='int64',
+                g='int64',
+                h='int64',
+                i='int64',
+                j='int64',
+                k='int64',
+                l='int64',
+                m='int64',
+                n='int64',
+                o='int64',
+                p='int64',
+            ):
+                (
+                    a,
+                    b,
+                    c,
+                    d,
+                    e,
+                    f,
+                    g,
+                    h,
+                    i,
+                    j,
+                    k,
+                    l,
+                    m,
+                    n,
+                    o,
+                    p
+                ) = py_func()
+            return (
+                a
+                + b
+                + c
+                + d
+                + e
+                + f
+                + g
+                + h
+                + i
+                + j
+                + k
+                + l
+                + m
+                + n
+                + o
+                + p
+            )
+
+        a = sum(list(py_func()))
+        b = objmode_func()
+        self.assertEqual(a, b)
+
+    @skip_unless_py10
+    def test_large_args_inline_controlflow(self):
+        """
+        Tests generating large args when one of the inputs
+        has inlined controlflow.
+        """
+        def inline_func(flag):
+            return sum_jit_func(
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1 if flag else 2,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                arg41=1,
+            )
+
+        with self.assertRaises(UnsupportedError) as raises:
+            njit()(inline_func)(False)
+        self.assertIn(
+            'You can resolve this issue by moving the control flow out',
+            str(raises.exception)
+        )
+
+    @skip_unless_py10
+    def test_large_args_noninlined_controlflow(self):
+        """
+        Tests generating large args when one of the inputs
+        has the change suggested in the error message
+        for inlined control flow.
+        """
+        def inline_func(flag):
+            a_val = 1 if flag else 2
+            return sum_jit_func(
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                a_val,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                arg41=1,
+            )
+
+        py_func = inline_func
+        cfunc = njit()(inline_func)
+        a = py_func(False)
+        b = cfunc(False)
+        self.assertEqual(a, b)
+
+    @skip_unless_py10
+    def test_all_args_inline_controlflow(self):
+        """
+        Tests generating only large args when one of the inputs
+        has inlined controlflow. This requires a special check
+        inside peep_hole_call_function_ex_to_call_function_kw
+        because it usually only handles varkwargs.
+        """
+        def inline_func(flag):
+            return sum_jit_func(
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1 if flag else 2,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+            )
+
+        with self.assertRaises(UnsupportedError) as raises:
+            njit()(inline_func)(False)
+        self.assertIn(
+            'You can resolve this issue by moving the control flow out',
+            str(raises.exception)
+        )
+
+    @skip_unless_py10
+    def test_all_args_noninlined_controlflow(self):
+        """
+        Tests generating large args when one of the inputs
+        has the change suggested in the error message
+        for inlined control flow.
+        """
+        def inline_func(flag):
+            a_val = 1 if flag else 2
+            return sum_jit_func(
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                a_val,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+            )
+
+        py_func = inline_func
+        cfunc = njit()(inline_func)
+        a = py_func(False)
+        b = cfunc(False)
+        self.assertEqual(a, b)
+
+    @skip_unless_py10
+    def test_large_kws_inline_controlflow(self):
+        """
+        Tests generating large kws when one of the inputs
+        has inlined controlflow.
+        """
+        def inline_func(flag):
+            return sum_jit_func(
+                arg0=1,
+                arg1=1,
+                arg2=1,
+                arg3=1,
+                arg4=1,
+                arg5=1,
+                arg6=1,
+                arg7=1,
+                arg8=1,
+                arg9=1,
+                arg10=1,
+                arg11=1,
+                arg12=1,
+                arg13=1,
+                arg14=1,
+                arg15=1 if flag else 2,
+            )
+
+        with self.assertRaises(UnsupportedError) as raises:
+            njit()(inline_func)(False)
+        self.assertIn(
+            'You can resolve this issue by moving the control flow out',
+            str(raises.exception)
+        )
+
+    @skip_unless_py10
+    def test_large_kws_noninlined_controlflow(self):
+        """
+        Tests generating large kws when one of the inputs
+        has the change suggested in the error message
+        for inlined control flow.
+        """
+        def inline_func(flag):
+            a_val = 1 if flag else 2
+            return sum_jit_func(
+                arg0=1,
+                arg1=1,
+                arg2=1,
+                arg3=1,
+                arg4=1,
+                arg5=1,
+                arg6=1,
+                arg7=1,
+                arg8=1,
+                arg9=1,
+                arg10=1,
+                arg11=1,
+                arg12=1,
+                arg13=1,
+                arg14=1,
+                arg15=a_val,
+            )
+
+        py_func = inline_func
+        cfunc = njit()(inline_func)
+        a = py_func(False)
+        b = cfunc(False)
+        self.assertEqual(a, b)