Repository: numba/numba

Issue:
Crash involving `typed.Dict`, caching, and parametrized type
- [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/master/CHANGE_LOG).
- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

Likely needs #6336 fixed to start, ie requires `numba>0.51`.

Sorry, this is still a little vague but since I only have Windows boxes debugging crashes is not easy. The script below crashes when run more than once, ie when data is loaded from cache. It no longer crashes when caching is turned off (`@jit(..., cache=False)` on line 64), unique type names are used (line 14), or `doit(x, y)` is called with arguments of the same type (`typeof(x) == typeof(y)` on line 65).

```python
from numba import generated_jit, jit, typed, types
from numba.core.datamodel.models import UniTupleModel
from numba.extending import box, register_model, typeof_impl, unbox


class Parametrized(tuple):
    def __init__(self, tup):
        assert all(isinstance(v, str) for v in tup)  # the actual work is already done by __new__


class ParametrisedType(types.Type):
    """this is essentially UniTuple(unicode_type, n) BUT type name is the same for all n"""
    def __init__(self, value):
        super(ParametrisedType, self).__init__('ParametrisedType')  # <- using a unique name (like f'ParametrisedType({value})') here will not crash
        self.dtype = types.unicode_type
        self.n = len(value)

    @property
    def key(self):
        return self.n

    def __len__(self):
        return self.n


register_model(ParametrisedType)(UniTupleModel)


@typeof_impl.register(Parametrized)
def typeof_unit(val, c):
    return ParametrisedType(val)


@unbox(ParametrisedType)
def unbox_parametrized(typ, obj, context):
    return context.unbox(types.UniTuple(typ.dtype, len(typ)), obj)

# reproducer currently does not need this
# @box(ParametrisedType)
# def box_parametrized(typ, val, context):
#     tup = context.box(types.UniTuple(typ.dtype, len(typ)), val)
#     cls = context.pyapi.unserialize(context.pyapi.serialize_object(Parametrized))
#     obj = context.pyapi.call_function_objargs(cls, (tup,))
#     context.pyapi.decref(tup)
#     return obj


@generated_jit
def dict_vs_cache_vs_parametrized(v):
    typ = v

    def dict_vs_cache_vs_parametrized_impl(v):
        # typed.List shows same behaviour after fix for #6397
        d = typed.Dict.empty(types.unicode_type, typ)
        d['data'] = v

    return dict_vs_cache_vs_parametrized_impl


def test_dict_vs_cache_vs_parametrized():
    # crashes when run more than once (ie when compiled function is loaded from cache)
    x, y = Parametrized(('horst', 'hanz')), Parametrized(('horst',))

    @jit(nopython=True, cache=True)
    def doit(x, y):  # <- does not crash when typeof(x) == typeof(y)
        dict_vs_cache_vs_parametrized(x)
        dict_vs_cache_vs_parametrized(y)

    for ii in range(50):  # <- somtimes works a few times
        assert doit(x, y) is None


if __name__ == '__main__':
    test_dict_vs_cache_vs_parametrized()
```


I have a hunch caching is not really to blame here. I'd suspect the non-unique type name is derailing code generation somehow, just have not found a reproducer w/o caching, yet.


Discussion:
@esc I think the issue might be [here](https://github.com/numba/numba/blob/3a682bd827e416335e3574bc7b10f0ec69adb701/numba/typed/typedobjectutils.py#L131) (same pattern [here](https://github.com/numba/numba/blob/3a682bd827e416335e3574bc7b10f0ec69adb701/numba/typed/typedobjectutils.py#L143), [here](https://github.com/numba/numba/blob/3a682bd827e416335e3574bc7b10f0ec69adb701/numba/typed/typedobjectutils.py#L180), [here](https://github.com/numba/numba/blob/3a682bd827e416335e3574bc7b10f0ec69adb701/numba/typed/typedobjectutils.py#L187), [here](https://github.com/numba/numba/blob/3a682bd827e416335e3574bc7b10f0ec69adb701/numba/cpython/listobj.py#L281)). If `str(fe_type_1) == str(fe_type_2) ` but `fe_type_1 != fe_type_2` (which is what is happening with `ParamtrizedType` above) you end-up with incref functions for `fe_type_1` and `fe_type_2` with the same name. Not sure how to fix this though as types are so short lived and there is no long-term stable (ie accross interpreter restarts) string invariant for types I am aware of. 

First few stack frames:

```
Thread 1 "python" received signal SIGSEGV, Segmentation fault.
0x00007ffff7fd6055 in NRT_incref ()
(gdb) bt
#0  0x00007ffff7fd6055 in NRT_incref ()
#1  0x00007ffff5e07cc1 in dk_incref_val (val=0x7fffffffbfe0 " j\344\367\377\177", dk=0x555556586520) at numba/cext/dictobject.c:465
#2  numba_dict_insert (oldval_bytes=<optimised out>, val_bytes=<optimised out>, hash=<optimised out>, key_bytes=<optimised out>, d=<optimised out>)
    at numba/cext/dictobject.c:725
#3  numba_dict_insert_ez (d=<optimised out>, key_bytes=<optimised out>, hash=<optimised out>, val_bytes=<optimised out>) at numba/cext/dictobject.c:1005
#4  0x00007ffff7dfdbfb in numba::typed::dictobject::impl_setitem::$3clocals$3e::impl$2434(DictType$5bunicode_type$2cParametrisedType$5d$3civ$3dNone$3e, unicode_type, ParametrisedType) ()
#5  0x00007ffff7dfd96e in __main__::dict_vs_cache_vs_parametrized::$3clocals$3e::dict_vs_cache_vs_parametrized_impl$2431(ParametrisedType) ()
#6  0x00007ffff7dfc1af in __main__::test_dict_vs_cache_vs_parametrized::$3clocals$3e::doit$241(ParametrisedType, ParametrisedType) ()
#7  0x00007ffff7dfc581 in cpython::__main__::test_dict_vs_cache_vs_parametrized::$3clocals$3e::doit$241(ParametrisedType, ParametrisedType) ()
#8  0x00007fff919edec1 in call_cfunc (locals=0x0, kws=0x0, args=(<Parametrized at remote 0x7fff91187a90>, <Parametrized at remote 0x7fff9191fd40>), 
    cfunc=<built-in method doit of _dynfunc._Closure object at remote 0x7fff902dab80>, self=0x7fff9117a6a0) at numba/_dispatcher.c:340
#9  compile_and_invoke (self=self@entry=0x7fff9117a6a0, args=args@entry=(<Parametrized at remote 0x7fff91187a90>, <Parametrized at remote 0x7fff9191fd40>), 
    kws=kws@entry=0x0, locals=locals@entry=0x0) at numba/_dispatcher.c:363
#10 0x00007fff919ee410 in Dispatcher_call (self=0x7fff9117a6a0, args=(<Parametrized at remote 0x7fff91187a90>, <Parametrized at remote 0x7fff9191fd40>), 
    kws=<optimised out>) at numba/_dispatcher.c:578
```

@asodeur I think this aligns with your theory about the source of the issue.
I think there is an escape hatch. The offending functions are created by intrinsics that are called exclusively by `new_dict`/`new_list`. Inside the intrinsics the mangled name of `new_dict`/`new_list` is available as `context.fndesc.mangled_name` and is unique for the `DictType`. So instead of creating functions `incref<key_type>`/`incref<value_type>` etc you could create `incref_key<dict_type>`/ `incref_value<dict_type>`. Naming for the latter is less problematic as the mangled name for `new_dict`/`new_list` is available.

Resolving patch:
diff --git a/numba/typed/dictobject.py b/numba/typed/dictobject.py
--- a/numba/typed/dictobject.py
+++ b/numba/typed/dictobject.py
@@ -283,9 +283,9 @@ def codegen(context, builder, sig, args):
 
         dm_key = context.data_model_manager[keyty.instance_type]
         if dm_key.contains_nrt_meminfo():
-            equal = _get_equal(context, builder.module, dm_key, 'dict')
+            equal = _get_equal(context, builder.module, dm_key, 'dict_key')
             key_incref, key_decref = _get_incref_decref(
-                context, builder.module, dm_key, 'dict'
+                context, builder.module, dm_key, 'dict_key'
             )
             builder.store(
                 builder.bitcast(equal, key_equal_ptr.type.pointee),
@@ -303,7 +303,7 @@ def codegen(context, builder, sig, args):
         dm_val = context.data_model_manager[valty.instance_type]
         if dm_val.contains_nrt_meminfo():
             val_incref, val_decref = _get_incref_decref(
-                context, builder.module, dm_val, 'dict'
+                context, builder.module, dm_val, 'dict_value'
             )
             builder.store(
                 builder.bitcast(val_incref, val_incref_ptr.type.pointee),
diff --git a/numba/typed/typedobjectutils.py b/numba/typed/typedobjectutils.py
--- a/numba/typed/typedobjectutils.py
+++ b/numba/typed/typedobjectutils.py
@@ -120,7 +120,7 @@ def _container_get_meminfo(context, builder, container_ty, c):
     return conatainer_struct.meminfo
 
 
-def _get_incref_decref(context, module, datamodel, container_type):
+def _get_incref_decref(context, module, datamodel, container_element_type):
     assert datamodel.contains_nrt_meminfo()
 
     fe_type = datamodel.fe_type
@@ -128,7 +128,8 @@ def _get_incref_decref(context, module, datamodel, container_type):
     refct_fnty = ir.FunctionType(ir.VoidType(), [data_ptr_ty])
     incref_fn = module.get_or_insert_function(
         refct_fnty,
-        name='.numba_{}_incref${}'.format(container_type, fe_type),
+        name='.numba_{}.{}_incref'.format(
+            context.fndesc.mangled_name, container_element_type),
     )
 
     builder = ir.IRBuilder(incref_fn.append_basic_block())
@@ -140,7 +141,8 @@ def _get_incref_decref(context, module, datamodel, container_type):
 
     decref_fn = module.get_or_insert_function(
         refct_fnty,
-        name='.numba_{}_decref${}'.format(container_type, fe_type),
+        name='.numba_{}.{}_decref'.format(
+            context.fndesc.mangled_name, container_element_type),
     )
     builder = ir.IRBuilder(decref_fn.append_basic_block())
     context.nrt.decref(
@@ -152,7 +154,7 @@ def _get_incref_decref(context, module, datamodel, container_type):
     return incref_fn, decref_fn
 
 
-def _get_equal(context, module, datamodel, container_type):
+def _get_equal(context, module, datamodel, container_element_type):
     assert datamodel.contains_nrt_meminfo()
 
     fe_type = datamodel.fe_type
@@ -177,14 +179,16 @@ def build_wrapper(fn):
 
     wrapfn = module.get_or_insert_function(
         wrapfnty,
-        name='.numba_{}_item_equal.wrap${}'.format(container_type, fe_type)
+        name='.numba_{}.{}_equal.wrap'.format(
+            context.fndesc.mangled_name, container_element_type)
     )
     build_wrapper(wrapfn)
 
     equal_fnty = ir.FunctionType(ir.IntType(32), [data_ptr_ty, data_ptr_ty])
     equal_fn = module.get_or_insert_function(
         equal_fnty,
-        name='.numba_{}_item_equal${}'.format(container_type, fe_type),
+        name='.numba_{}.{}_equal'.format(
+            context.fndesc.mangled_name, container_element_type),
     )
     builder = Builder(equal_fn.append_basic_block())
     lhs = datamodel.load_from_data_pointer(builder, equal_fn.args[0])


Test patch:
diff --git a/numba/tests/test_dictimpl.py b/numba/tests/test_dictimpl.py
--- a/numba/tests/test_dictimpl.py
+++ b/numba/tests/test_dictimpl.py
@@ -6,8 +6,10 @@
 import random
 
 from numba.tests.support import TestCase
-from numba import _helperlib
+from numba import generated_jit, _helperlib, jit, typed, types
 from numba.core.config import IS_32BITS
+from numba.core.datamodel.models import UniTupleModel
+from numba.extending import register_model, typeof_impl, unbox
 
 
 DKIX_EMPTY = -1
@@ -181,6 +183,30 @@ def __next__(self):
     next = __next__    # needed for py2 only
 
 
+class Parametrized(tuple):
+    """supporting type for TestDictImpl.test_parametrized_types
+    needs to be global to be cacheable"""
+    def __init__(self, tup):
+        assert all(isinstance(v, str) for v in tup)
+
+
+class ParametrizedType(types.Type):
+    """this is essentially UniTuple(unicode_type, n)
+    BUT type name is the same for all n"""
+
+    def __init__(self, value):
+        super(ParametrizedType, self).__init__('ParametrizedType')
+        self.dtype = types.unicode_type
+        self.n = len(value)
+
+    @property
+    def key(self):
+        return self.n
+
+    def __len__(self):
+        return self.n
+
+
 class TestDictImpl(TestCase):
     def setUp(self):
         """Bind to the c_helper library and provide the ctypes wrapper.
@@ -577,3 +603,49 @@ def test_sizing(self):
         # Check different sizes of the key & value.
         for i in range(1, 8):
             self.check_sizing(key_size=i, val_size=i, nmax=2**i)
+
+    def test_parametrized_types(self):
+        """https://github.com/numba/numba/issues/6401"""
+
+        register_model(ParametrizedType)(UniTupleModel)
+
+        @typeof_impl.register(Parametrized)
+        def typeof_unit(val, c):
+            return ParametrizedType(val)
+
+        @unbox(ParametrizedType)
+        def unbox_parametrized(typ, obj, context):
+            return context.unbox(types.UniTuple(typ.dtype, len(typ)), obj)
+
+        @generated_jit
+        def dict_vs_cache_vs_parametrized(v):
+            typ = v
+
+            def objmode_vs_cache_vs_parametrized_impl(v):
+                # typed.List shows same behaviour after fix for #6397
+                d = typed.Dict.empty(types.unicode_type, typ)
+                d['data'] = v
+
+            return objmode_vs_cache_vs_parametrized_impl
+
+        @jit(nopython=True, cache=True)
+        def set_parametrized_data(x, y):
+            # Has had a tendency to segfault when the compiled function
+            # was loaded from cache in a different process than the one
+            # it was originally compiled in.
+            # The new process is simulated below by resetting the dispatchers
+            # and the target context
+            dict_vs_cache_vs_parametrized(x)
+            dict_vs_cache_vs_parametrized(y)
+
+        x, y = Parametrized(('a', 'b')), Parametrized(('a',))
+        set_parametrized_data(x, y)
+
+        # reset dispatchers and targetctx to force re-load from cache as if a
+        # new process would jit the function
+        set_parametrized_data._make_finalizer()()
+        set_parametrized_data._reset_overloads()
+        set_parametrized_data.targetctx.init()
+
+        for ii in range(50):  # <- somtimes works a few times
+            self.assertIsNone(set_parametrized_data(x, y))