Repository: numba/numba

Issue:
np.full ignores keyword argument names
<!--

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).

-->

## In `np.full`, numba ignores the named keyword arguments. 

```python
import numba
import numpy as np

@numba.jit(nopython=True)
def test1():
    x = np.full(shape=123, fill_value=456)
    print(x.shape)

@numba.jit(nopython=True)
def test2():
    x = np.full(fill_value=456, shape=123)
    print(x.shape)

test1()
test2()
```

outputs

```python
(123,)
(456,)  # WRONG! The shape is given as `shape=123` in the function call!
```

## Bug

Both `test1` and `test2` would be interpereted the same by numpy alone. The only difference between them is the order of the keyword arguments `fill_value` and `shape`. Numba seems to silently ignore this order. I am not sure this is a bug, but this just cost me nearly one hour of debugging. (I am still a very happy Numba user)

## Numba version

```numba                0.55.1```







<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/main/CHANGE_LOG).
- [x] I have included a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->



Discussion:
Thanks for the report @charelF. I can reproduce the error.

Edit: I guess the error is at the stub generator step which doesn't consider keyword arguments. I changed the implementation of `np.full` to return the shape instead of an array. Below is the IR dump for the test reported above.

```javascript
---------------------------------IR DUMP: test1---------------------------------
label 0:
    $2load_global.0 = global(np: <module 'numpy' from '/home/guilhermeleobas/miniconda3/envs/numba/lib/python3.8/site-packages/numpy/__init__.py'>) ['$2load_global.0']
    $4load_attr.1 = getattr(value=$2load_global.0, attr=full) ['$2load_global.0', '$4load_attr.1']
    $const6.2 = const(int, 123)              ['$const6.2']
    $const8.3 = const(int, 456)              ['$const8.3']
    x = call $4load_attr.1(func=$4load_attr.1, args=[], kws=[('shape', Var($const6.2, a.py:7)), ('fill_value', Var($const8.3, a.py:7))], vararg=None, target=None) ['$4load_attr.1', '$const6.2', '$const8.3', 'x']
    $18return_value.7 = cast(value=x)        ['$18return_value.7', 'x']
    return $18return_value.7                 ['$18return_value.7']

-------------IR DUMP: jit_wrapper__function_full_at_0x7ff548b88700_-------------
label 0:
    shape = arg(0, name=shape)               ['shape']
    fill_value = arg(1, name=fill_value)     ['fill_value']
    $2load_global.0 = global(intrin: <intrinsic stub>) ['$2load_global.0']
    $8call_function.3 = call $2load_global.0(shape, fill_value, func=$2load_global.0, args=[Var(shape, <string>:3), Var(fill_value, <string>:3)], kws=(), vararg=None, target=None) ['$2load_global.0', '$8call_function.3', 'fill_value', 'shape']
    $10return_value.4 = cast(value=$8call_function.3) ['$10return_value.4', '$8call_function.3']
    return $10return_value.4                 ['$10return_value.4']

----------------------IR DUMP: numpy_full_nd.<locals>.full----------------------
label 0:
    shape = arg(0, name=shape)               ['shape']
    value = arg(1, name=value)               ['value']
    $4return_value.1 = cast(value=shape)     ['$4return_value.1', 'shape']
    return $4return_value.1                  ['$4return_value.1']
```

```javascript
---------------------------------IR DUMP: test2---------------------------------
label 0:
    $2load_global.0 = global(np: <module 'numpy' from '/home/guilhermeleobas/miniconda3/envs/numba/lib/python3.8/site-packages/numpy/__init__.py'>) ['$2load_global.0']
    $4load_attr.1 = getattr(value=$2load_global.0, attr=full) ['$2load_global.0', '$4load_attr.1']
    $const6.2 = const(int, 456)              ['$const6.2']
    $const8.3 = const(int, 123)              ['$const8.3']
    x = call $4load_attr.1(func=$4load_attr.1, args=[], kws=[('fill_value', Var($const6.2, a.py:13)), ('shape', Var($const8.3, a.py:13))], vararg=None, target=None) ['$4load_attr.1', '$const6.2', '$const8.3', 'x']
    $18return_value.7 = cast(value=x)        ['$18return_value.7', 'x']
    return $18return_value.7                 ['$18return_value.7']

-------------IR DUMP: jit_wrapper__function_full_at_0x7ff548b88700_-------------
label 0:
    fill_value = arg(0, name=fill_value)     ['fill_value']
    shape = arg(1, name=shape)               ['shape']
    $2load_global.0 = global(intrin: <intrinsic stub>) ['$2load_global.0']
    $8call_function.3 = call $2load_global.0(fill_value, shape, func=$2load_global.0, args=[Var(fill_value, <string>:3), Var(shape, <string>:3)], kws=(), vararg=None, target=None) ['$2load_global.0', '$8call_function.3', 'fill_value', 'shape']
    $10return_value.4 = cast(value=$8call_function.3) ['$10return_value.4', '$8call_function.3']
    return $10return_value.4                 ['$10return_value.4']

----------------------IR DUMP: numpy_full_nd.<locals>.full----------------------
label 0:
    shape = arg(0, name=shape)               ['shape']
    value = arg(1, name=value)               ['value']
    $4return_value.1 = cast(value=shape)     ['$4return_value.1', 'shape']
    return $4return_value.1                  ['$4return_value.1']
```

Notice that at the second call, the intrinsic stub doesn't switch `fill_value` and `shape`.
I think the problem is in the following code where we define `full` function and compile it later. It always assumes that `args[0]` is `shape` and `args[1]` is `fill_value`. I applied the diff (see below) and then it started giving correct results.

https://github.com/numba/numba/blob/63837c7e557db841ad8523b959798635fc7a4ddf/numba/np/arrayobj.py#L3882-L3892

```diff
diff --git a/numba/np/arrayobj.py b/numba/np/arrayobj.py
index 6d5d2f042..25894b5dc 100644
--- a/numba/np/arrayobj.py
+++ b/numba/np/arrayobj.py
@@ -3881,15 +3881,27 @@ def numpy_zeros_like_nd(context, builder, sig, args):
 @glue_lowering(np.full, types.Any, types.Any)
 def numpy_full_nd(context, builder, sig, args):
 
-    def full(shape, value):
-        arr = np.empty(shape, type(value))
-        arr_flat = arr.flat
-        for idx in range(len(arr_flat)):
-            arr_flat[idx] = value
-        return arr
+    if "shape" in args[0].name and "fill_value" in args[1].name:
+        def full(shape, value):
+            arr = np.empty(shape, type(value))
+            arr_flat = arr.flat
+            for idx in range(len(arr_flat)):
+                arr_flat[idx] = value
+            return arr
+
+        res = context.compile_internal(builder, full, sig, args)
+        return impl_ret_new_ref(context, builder, sig.return_type, res)
+    elif "fill_value" in args[0].name and "shape" in args[1].name:
+        def full(value, shape):
+            arr = np.empty(shape, type(value))
+            arr_flat = arr.flat
+            for idx in range(len(arr_flat)):
+                arr_flat[idx] = value
+            return arr
+
+        res = context.compile_internal(builder, full, sig, args)
+        return impl_ret_new_ref(context, builder, sig.return_type, res)
 
-    res = context.compile_internal(builder, full, sig, args)
-    return impl_ret_new_ref(context, builder, sig.return_type, res)
 
 
 @glue_lowering(np.full, types.Any, types.Any, types.DTypeSpec)
```
This is probably `@overload_glue` related, it worked fine until version 0.54. Also, the use of the standard `@overload` works fine:
```python
from numba import njit
import numpy as np
from numba.extending import overload

# This is ok
@njit
def foo(x, y):
    print(x, y)

foo(x=1, y=2j)
foo(y=3j, x=4)

def func(a, b):
    pass

# This is also ok
@overload(func)
def ol_func(a, b):
    def impl(a, b):
        print(a, b)
    return impl

@njit
def bar(x, y):
    func(a=x, b=y)
    func(b=y, a=x)

bar(1, 2j)
```
Think the problem is here:
https://github.com/numba/numba/blob/63837c7e557db841ad8523b959798635fc7a4ddf/numba/core/overload_glue.py#L157-L162
the order of the keys has to match the lowering, but that isn't known at this point (it probably can be known, it's just not written like that). It's also currently relying on insertion order preservation, which is not ideal.

Resolving patch:
diff --git a/numba/core/overload_glue.py b/numba/core/overload_glue.py
--- a/numba/core/overload_glue.py
+++ b/numba/core/overload_glue.py
@@ -27,28 +27,26 @@ def __init__(self, function, typing_key=None):
         # is all lazy.
         self._build()
 
-    def _stub_generator(self, nargs, body_func, kwargs=None):
-        """This generates a function that takes "nargs" count of arguments
-        and the presented kwargs, the "body_func" is the function that'll
-        type the overloaded function and then work out which lowering to
-        return"""
+    def _stub_generator(self, body_func, varnames):
+        """This generates a function based on the argnames provided in
+        "varnames", the "body_func" is the function that'll type the overloaded
+        function and then work out which lowering to return"""
         def stub(tyctx):
             # body is supplied when the function is magic'd into life via glbls
             return body(tyctx)  # noqa: F821
-        if kwargs is None:
-            kwargs = {}
-        # create new code parts
-        stub_code = stub.__code__
-        co_args = [stub_code.co_argcount + nargs + len(kwargs)]
 
+        stub_code = stub.__code__
         new_varnames = [*stub_code.co_varnames]
-        new_varnames.extend([f'tmp{x}' for x in range(nargs)])
-        new_varnames.extend([x for x, _ in kwargs.items()])
+        new_varnames.extend(varnames)
+        co_argcount = len(new_varnames)
+        co_args = [co_argcount]
+        additional_co_nlocals = len(varnames)
+
         from numba.core import utils
         if utils.PYVERSION >= (3, 8):
             co_args.append(stub_code.co_posonlyargcount)
         co_args.append(stub_code.co_kwonlyargcount)
-        co_args.extend([stub_code.co_nlocals + nargs + len(kwargs),
+        co_args.extend([stub_code.co_nlocals + additional_co_nlocals,
                         stub_code.co_stacksize,
                         stub_code.co_flags,
                         stub_code.co_code,
@@ -131,7 +129,6 @@ def _build(self):
         @overload(self._function, strict=False,
                   jit_options={'forceinline': True})
         def ol_generated(*ol_args, **ol_kwargs):
-
             def body(tyctx):
                 msg = f"No typer registered for {self._function}"
                 if self._TYPER is None:
@@ -151,16 +148,65 @@ def body(tyctx):
                     raise errors.InternalError(msg)
                 return sig, lowering
 
-            stub = self._stub_generator(len(ol_args), body, ol_kwargs)
+            # Need a typing context now so as to get a signature and a binding
+            # for the kwarg order.
+            from numba.core.target_extension import (dispatcher_registry,
+                                                     resolve_target_str,
+                                                     current_target)
+            disp = dispatcher_registry[resolve_target_str(current_target())]
+            typing_context = disp.targetdescr.typing_context
+            typing = self._TYPER(typing_context)
+            sig = typing.apply(ol_args, ol_kwargs)
+            if not sig:
+                # No signature is a typing error, there's no match, so report it
+                raise errors.TypingError("No match")
+
+            # The following code branches based on whether the signature has a
+            # "pysig", if it does, it's from a CallableTemplate and
+            # specialisation is required based on precise arg/kwarg names and
+            # default values, if it does not, then it just requires
+            # specialisation based on the arg count.
+            #
+            # The "gen_var_names" function is defined to generate the variable
+            # names at the call site of the intrinsic.
+            #
+            # The "call_str_specific" is the list of args to the function
+            # returned by the @overload, it has to have matching arg names and
+            # kwargs names/defaults if the underlying typing template supports
+            # it (CallableTemplate), else it has to have a matching number of
+            # arguments (AbstractTemplate). The "call_str" is the list of args
+            # that will be passed to the intrinsic that deals with typing and
+            # selection of the lowering etc, so it just needs to be a list of
+            # the argument names.
+
+            if sig.pysig: # CallableTemplate, has pysig
+                pysig_params = sig.pysig.parameters
+
+                # Define the var names
+                gen_var_names = [x for x in pysig_params.keys()]
+                # CallableTemplate, pysig is present so generate the exact thing
+                # this is to permit calling with positional args specified by
+                # name.
+                buf = []
+                for k, v in pysig_params.items():
+                    if v.default is v.empty: # no default ~= positional arg
+                        buf.append(k)
+                    else: # is kwarg, wire in default
+                        buf.append(f'{k} = {v.default}')
+                call_str_specific = ', '.join(buf)
+                call_str = ', '.join(pysig_params.keys())
+            else: # AbstractTemplate, need to bind 1:1 vars to the arg count
+                # Define the var names
+                gen_var_names = [f'tmp{x}' for x in range(len(ol_args))]
+                # Everything is just passed by position, there should be no
+                # kwargs.
+                assert not ol_kwargs
+                call_str_specific = ', '.join(gen_var_names)
+                call_str = call_str_specific
+
+            stub = self._stub_generator(body, gen_var_names)
             intrin = intrinsic(stub)
 
-            # This is horrible, need to generate a jit wrapper function that
-            # walks the ol_kwargs into the intrin with a signature that
-            # matches the lowering sig. The actual kwarg var names matter,
-            # they have to match exactly.
-            arg_str = ','.join([f'tmp{x}' for x in range(len(ol_args))])
-            kws_str = ','.join(ol_kwargs.keys())
-            call_str = ','.join([x for x in (arg_str, kws_str) if x])
             # NOTE: The jit_wrapper functions cannot take `*args`
             # albeit this an obvious choice for accepting an unknown number
             # of arguments. If this is done, `*args` ends up as a cascade of
@@ -174,7 +220,7 @@ def body(tyctx):
             gen = textwrap.dedent(("""
             def jit_wrapper_{}({}):
                 return intrin({})
-            """)).format(name, call_str, call_str)
+            """)).format(name, call_str_specific, call_str)
             l = {}
             g = {'intrin': intrin}
             exec(gen, g, l)
diff --git a/numba/core/typing/npydecl.py b/numba/core/typing/npydecl.py
--- a/numba/core/typing/npydecl.py
+++ b/numba/core/typing/npydecl.py
@@ -1254,7 +1254,7 @@ class NumbaCArray(CallableTemplate):
     def generic(self):
         func_name = self.key.__name__
 
-        def typer(ptr, shape, dtype=types.none):
+        def typer(ptr, shape, dtype=None):
             if ptr is types.voidptr:
                 ptr_dtype = None
             elif isinstance(ptr, types.CPointer):
@@ -1263,7 +1263,7 @@ def typer(ptr, shape, dtype=types.none):
                 raise NumbaTypeError("%s(): pointer argument expected, got '%s'"
                                      % (func_name, ptr))
 
-            if dtype is types.none:
+            if dtype is None:
                 if ptr_dtype is None:
                     raise NumbaTypeError("%s(): explicit dtype required for void* argument"
                                          % (func_name,))


Test patch:
diff --git a/numba/tests/test_extending.py b/numba/tests/test_extending.py
--- a/numba/tests/test_extending.py
+++ b/numba/tests/test_extending.py
@@ -1957,6 +1957,20 @@ def foo(x):
             is_jitted(guvectorize("void(float64[:])", "(m)")(foo))
         )
 
+    def test_overload_glue_arg_binding(self):
+        # See issue #7982, checks that calling a function with named args works
+        # correctly irrespective of the order in which the names are supplied.
+        @njit
+        def standard_order():
+            return np.full(shape=123, fill_value=456).shape
+
+        @njit
+        def reversed_order():
+            return np.full(fill_value=456, shape=123).shape
+
+        self.assertPreciseEqual(standard_order(), standard_order.py_func())
+        self.assertPreciseEqual(reversed_order(), reversed_order.py_func())
+
 
 class TestOverloadPreferLiteral(TestCase):
     def test_overload(self):