Repository: numba/numba

Issue:
Further problems with literally in overload
I saw that @sklam fixed #5011 with #5018. I believe I have stumbled across another problem with `literally` in `@overload` present in 0.47 and 0.48dev0.

The implementations of numpy functions with `axis` parameters use an idom like the following:
```python
if axis == 0:
    index_tuple0 = _gen_index_tuple(arr.shape, axis_index, 0)
    # snip
elif axis == 1:
    index_tuple0 = _gen_index_tuple(arr.shape, axis_index, 1)
    # snip
```

I thought this would now no longer be necessary but could be replaced by a common implementation for all axis values like so:
```python
axis_val = literally(axis)
index_tuple = _gen_index_tuple(arr.shape, axis_index, axis_val)
```

Unfortunately that fails to compile with the message:
```
TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Internal error at .
bind() argument after ** must be a mapping, not list
[1] During: resolving callee type: Function()
[2] During: typing of call at  (121)

Enable logging at debug level for details.

File "", line 121:
    def test_numba_axis_max(arr, axis):
        return np.max(arr, axis=axis)
        ^
```

Should this work or am I misusing `literally`?

Reproducer in the details below:

<details>

```python
import numba.types as types
import numpy as np
from numba import literally, njit
from numba.extending import overload, overload_method, register_jitable
from numba.targets.arraymath import _gen_index_tuple, zero_dim_msg


@register_jitable
def _array_max_binary_op(operand1_in_out, operand2_in, inplace=False):
    assert operand1_in_out.shape == operand2_in.shape
    if inplace:
        result = operand1_in_out
    else:
        result = np.copy(operand1_in_out)

    out = result.flat
    in2 = operand2_in.flat
    for i in range(operand1_in_out.size):
        if in2[i] > out[i]:
            out[i] = in2[i]
    return result


def gen_max_axis_impl(is_axis_const, const_axis_val):
    def array_max_axis_impl(arr, axis=None):
        """
        function that finds the maximum over one specific axis

        The third parameter to gen_index_tuple that generates the indexing
        tuples has to be a const so we can't just pass "axis" through since
        that isn't const.  We can check for specific values and have
        different instances that do take consts.  Supporting axis summation
        only up to the fourth dimension for now.

        typing/arraydecl.py:sum_expand defines the return type for sum with
        axis. It is one dimension less than the input array.
        """
        ndim = arr.ndim

        if not is_axis_const:
            # Catch where axis is negative or greater than 3.
            if axis < 0 or axis > 3:
                raise ValueError(
                    "Numba does not support sum with axis "
                    "parameter outside the range 0 to 3."
                )

        # Catch the case where the user misspecifies the axis to be
        # more than the number of the array's dimensions.
        if axis >= ndim:
            raise ValueError("axis is out of bounds for array")

        # Convert the shape of the input array to a list.
        ashape = list(arr.shape)
        # Get the length of the axis dimension.
        axis_len = ashape[axis]
        # # Remove the axis dimension from the list of dimensional lengths.
        # ashape.pop(axis)
        # # Convert this shape list back to a tuple using above intrinsic.
        # ashape_without_axis = _create_tuple_result_shape(ashape, arr.shape)
        # # Tuple needed here to create output array with correct size.
        # result = np.full(ashape_without_axis, zero, type(zero))

        if is_axis_const:
            # constant specialized version works for any valid axis value
            index_tuple_generic = _gen_index_tuple(arr.shape, 0, 0)
            result = np.copy(arr[index_tuple_generic])
            for axis_index in range(1, axis_len):
                index_tuple_generic = _gen_index_tuple(
                    arr.shape, axis_index, const_axis_val
                )
                result = _array_max_binary_op(result, arr[index_tuple_generic])
        else:
            axis_val = literally(axis)
            index_tuple0 = _gen_index_tuple(arr.shape, 0, axis_val)
            result = np.copy(arr[index_tuple0])
            for axis_index in range(1, axis_len):
                # Generate a tuple used to index the input array.
                # The tuple is ":" in all dimensions except the axis
                # dimension where it is "axis_index".
                index_tuple0 = _gen_index_tuple(arr.shape, axis_index, axis_val)
                result = _array_max_binary_op(result, arr[index_tuple0])
        return result

    return array_max_axis_impl


@overload(np.max)
@overload_method(types.Array, "max")
def array_max(arr, axis=None):
    if not isinstance(arr, types.Array):
        return

    if axis is None or isinstance(axis, types.Omitted):
        return
    elif isinstance(axis, types.IntegerLiteral):
        # this special-cases for constant axis
        const_axis_val = axis.literal_value
        # fix negative axis
        if const_axis_val < 0:
            const_axis_val = arr.ndim + const_axis_val
        if const_axis_val < 0 or const_axis_val > arr.ndim:
            raise ValueError("'axis' entry is out of bounds")
        is_axis_const = True
    elif isinstance(axis, types.Integer):
        is_axis_const = False
        const_axis_val = 0
    else:
        return
    return gen_max_axis_impl(is_axis_const, const_axis_val)


if __name__ == "__main__":
    ### test max(arr, axis) implementation
    test_dims = [3, 3, 3]
    a = np.random.rand(np.prod(test_dims)).reshape(test_dims)


    @njit(cache=True)
    def test_numba_axis_max(arr, axis):
        return np.max(arr, axis=axis)


    assert np.all(np.max(a, axis=0) == test_numba_axis_max(a, 0))

```

</details>


Discussion:
Thanks for the report. Looks like `literally` is triggering a mismatch to `pysig.bind`, `bind_kws` is a list when a mapping is expected, just stepped in with `pdb`, either making the list a dict or just doing `*bind_kws` permits this to get a bit further through compilation. Will need to find the root cause.
Just FYI that's the issue with literally we are affected by. In our case we are trying to call numpy's argsort with kind=literally(kind) argument from the overload_method implementation.
@sklam @stuartarchibald any progress here?
This becoming a blocking issue for us.

Resolving patch:
diff --git a/numba/typing/templates.py b/numba/typing/templates.py
--- a/numba/typing/templates.py
+++ b/numba/typing/templates.py
@@ -7,6 +7,7 @@
 import inspect
 import os.path
 from collections import namedtuple
+from collections.abc import Sequence
 from types import MethodType, FunctionType
 
 import numba
@@ -147,6 +148,9 @@ def fold_arguments(pysig, args, kws, normal_handler, default_handler,
     - default_handler(index, param, default) is called for omitted arguments
     - stararg_handler(index, param, values) is called for a "*args" argument
     """
+    if isinstance(kws, Sequence):
+        # Normalize dict kws
+        kws = dict(kws)
 
     # deal with kwonly args
     params = pysig.parameters


Test patch:
diff --git a/numba/tests/test_typeinfer.py b/numba/tests/test_typeinfer.py
--- a/numba/tests/test_typeinfer.py
+++ b/numba/tests/test_typeinfer.py
@@ -6,6 +6,7 @@
 from numba import unittest_support as unittest
 from numba.compiler import compile_isolated
 from numba import types, typeinfer, typing, jit, errors
+from numba import utils
 from numba.typeconv import Conversion
 
 from .support import TestCase, tag
@@ -678,5 +679,60 @@ def test_issue_1394(self):
             self.assertEqual(res, pyfunc(v))
 
 
+class TestFoldArguments(unittest.TestCase):
+    def check_fold_arguments_list_inputs(self, func, args, kws):
+        def make_tuple(*args):
+            return args
+
+        unused_handler = None
+
+        pysig = utils.pysignature(func)
+        names = list(pysig.parameters)
+
+        with self.subTest(kind='dict'):
+            folded_dict = typing.fold_arguments(
+                pysig, args, kws, make_tuple, unused_handler, unused_handler,
+            )
+            # correct ordering
+            for i, (j, k) in enumerate(zip(folded_dict, names)):
+                (got_index, got_param, got_name) = j
+                self.assertEqual(got_index, i)
+                self.assertEqual(got_name, f'arg.{k}')
+
+        kws = list(kws.items())
+        with self.subTest(kind='list'):
+            folded_list = typing.fold_arguments(
+                pysig, args, kws, make_tuple, unused_handler, unused_handler,
+            )
+            self.assertEqual(folded_list, folded_dict)
+
+    def test_fold_arguments_list_inputs(self):
+        cases = [
+            dict(
+                func=lambda a, b, c, d: None,
+                args=['arg.a', 'arg.b'],
+                kws=dict(c='arg.c', d='arg.d')
+            ),
+            dict(
+                func=lambda: None,
+                args=[],
+                kws=dict(),
+            ),
+            dict(
+                func=lambda a: None,
+                args=['arg.a'],
+                kws={},
+            ),
+            dict(
+                func=lambda a: None,
+                args=[],
+                kws=dict(a='arg.a'),
+            ),
+        ]
+        for case in cases:
+            with self.subTest(**case):
+                self.check_fold_arguments_list_inputs(**case)
+
+
 if __name__ == '__main__':
     unittest.main()