Repository: numba/numba

Issue:
Incorrect typing of something that should be a `LiteralStrKeyDict` and a typed Dict
Breaking out a separate issue from #6417 (cc @njriasan). The following:

```python
from numba import njit, void

@njit(void())
def f1():
    {'a': 1, 'b': 2.2}
```

results in the following:

```
< ... some TB omitted ... >
 File "/home/gmarkall/numbadev/numba/numba/core/compiler_machinery.py", line 262, in check
    mangled = func(compiler_state)
  File "/home/gmarkall/numbadev/numba/numba/core/typed_passes.py", line 94, in run_pass
    typemap, return_type, calltypes = type_inference_stage(
  File "/home/gmarkall/numbadev/numba/numba/core/typed_passes.py", line 72, in type_inference_stage
    infer.propagate(raise_errors=raise_errors)
  File "/home/gmarkall/numbadev/numba/numba/core/typeinfer.py", line 1071, in propagate
    raise errors[0]
numba.core.errors.TypingError: Failed in nopython mode pipeline (step: nopython mode backend)
Failed in nopython mode pipeline (step: nopython frontend)
No implementation of function Function(<built-in function setitem>) found for signature:
 
 >>> setitem(DictType[unicode_type,int64]<iv=None>, Literal[str](b), float64)
 
There are 16 candidate implementations:
  - Of which 14 did not match due to:
  Overload of function 'setitem': File: <numerous>: Line N/A.
    With argument(s): '(DictType[unicode_type,int64]<iv=None>, unicode_type, float64)':
   No match.
  - Of which 2 did not match due to:
  Overload in function 'impl_setitem': File: numba/typed/dictobject.py: Line 671.
    With argument(s): '(DictType[unicode_type,int64]<iv=None>, unicode_type, float64)':
   Rejected as the implementation raised a specific error:
     TypingError: Failed in nopython mode pipeline (step: nopython frontend)
   No implementation of function Function(<intrinsic _cast>) found for signature:
    
    >>> <unknown function>(float64, class(int64))
    
   There are 2 candidate implementations:
         - Of which 2 did not match due to:
         Intrinsic in function '_cast': File: numba/typed/typedobjectutils.py: Line 23.
           With argument(s): '(float64, class(int64))':
          Rejected as the implementation raised a specific error:
            TypingError: cannot safely cast float64 to int64. Please cast explicitly.
     raised from /home/gmarkall/numbadev/numba/numba/typed/typedobjectutils.py:76
   
   During: resolving callee type: Function(<intrinsic _cast>)
   During: typing of call at /home/gmarkall/numbadev/numba/numba/typed/dictobject.py (680)
   
   
   File "../../numba/numba/typed/dictobject.py", line 680:
       def impl(d, key, value):
           <source elided>
           castedkey = _cast(key, keyty)
           castedval = _cast(value, valty)
           ^

  raised from /home/gmarkall/numbadev/numba/numba/core/typeinfer.py:1071

During: typing of setitem at /home/gmarkall/numbadev/numba/numba/typed/dictobject.py (1106)

File "../../numba/numba/typed/dictobject.py", line 1106:
                def put(d, k, v):
                    d[k] = v
                    ^

During: lowering "$8build_const_key_map.5 = build_map(items=[(Var($8build_const_key_map.3, repro2.py:5), Var($const2.0, repro2.py:5)), (Var($8build_const_key_map.4, repro2.py:5), Var($const4.1, repro2.py:5))], size=2, literal_value={'a': 1, 'b': 2.2}, value_indexes={'a': 0, 'b': 1})" at repro2.py (5)
```

It appears that the dict which could (should?) be typed as a `LiteralStrKeyDict` was instead typed as a `typed.Dict` - the values are heterogeneous (`int64` and `float64`) which would be fine for a `LiteralStrKeyDict`, but the `typed.Dict` expects the values to be homogeneous in type.


Discussion:

Resolving patch:
diff --git a/numba/core/typeinfer.py b/numba/core/typeinfer.py
--- a/numba/core/typeinfer.py
+++ b/numba/core/typeinfer.py
@@ -27,6 +27,7 @@
                                new_error_context, termcolor, UnsupportedError,
                                ForceLiteralArg, CompilerError)
 from numba.core.funcdesc import qualifying_prefix
+from numba.core.typeconv import Conversion
 
 _logger = logging.getLogger(__name__)
 
@@ -337,12 +338,16 @@ def __call__(self, typeinfer):
                 strkey = all([isinstance(x, types.StringLiteral) for x in ktys])
                 literalvty = all([isinstance(x, types.Literal) for x in vtys])
                 vt0 = types.unliteral(vtys[0])
-                # homogeneous values comes in the form of being able to cast
-                # all the other values in the ctor to the type of the first
 
+                # homogeneous values comes in the form of being able to cast
+                # all the other values in the ctor to the type of the first.
+                # The order is important as `typed.Dict` takes it's type from
+                # the first element.
                 def check(other):
-                    return typeinfer.context.can_convert(other, vt0) is not None
+                    conv = typeinfer.context.can_convert(other, vt0)
+                    return conv is not None and conv < Conversion.unsafe
                 homogeneous = all([check(types.unliteral(x)) for x in vtys])
+
                 # Special cases:
                 # Single key:value in ctor, key is str, value is an otherwise
                 # illegal container type, e.g. LiteralStrKeyDict or


Test patch:
diff --git a/numba/tests/test_dictobject.py b/numba/tests/test_dictobject.py
--- a/numba/tests/test_dictobject.py
+++ b/numba/tests/test_dictobject.py
@@ -2145,10 +2145,10 @@ def value_type_mismatch(x):
     def test_dict_value_coercion(self):
         # checks that things coerce or not!
 
-        #    safe convertible: TypedDict
-        p = {(np.int32, np.int32): types.DictType,
-             # unsafe but convertible: TypedDict
-             (np.int8, np.int32): types.DictType,
+        p = {# safe and no conversion: TypedDict
+             (np.int32, np.int32): types.DictType,
+             # safe and convertible: TypedDict
+             (np.int32, np.int8): types.DictType,
              # safe convertible: TypedDict
              (np.complex128, np.int32): types.DictType,
              # unsafe not convertible: LiteralStrKey
@@ -2156,7 +2156,11 @@ def test_dict_value_coercion(self):
              # unsafe not convertible: LiteralStrKey
              (np.int32, np.array): types.LiteralStrKeyDict,
              # unsafe not convertible: LiteralStrKey
-             (np.array, np.int32): types.LiteralStrKeyDict,}
+             (np.array, np.int32): types.LiteralStrKeyDict,
+             # unsafe not convertible: LiteralStrKey
+             (np.int8, np.int32): types.LiteralStrKeyDict,
+             # unsafe not convertible: LiteralStrKey (issue #6420 case)
+             (np.int64, np.float64): types.LiteralStrKeyDict,}
 
         def bar(x):
             pass