Repository: numba/numba (Python)

Issue:
Typed List indices are cast unsafely
<!--

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).

-->

## Reporting a bug

<!--

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/master/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'.

```
import numba as nb
import numpy as np
@nb.njit
def f():
    l = nb.typed.List.empty_list(nb.uint8)
    for i in range(129):
        l.append(nb.uint8(i))
    a = l[-1]
    return l[a],l[nb.int8(a)],l[nb.uint16(a)]

f()
```
Returns (1,1,128) while I expected (128,1,128). 
Seems unsigned integers above 1/2 of the range are not good; so I'm guessing they are cast to the signed version with the same number of bits.

PS: what really threw me off is that this is not the behavior of numpy arrays
```
import numba as nb
@nb.njit
def f():
    N=129
    l = np.empty(N,dtype=np.uint8)
    for i in range(N):
        l[i]=nb.uint8(i)
    a = l[-1]
    return l[a],l[nb.int8(a)],l[nb.uint16(a)]

f()
```
Prints `(128,1,128)`.



Discussion:
@LunarLanding thank you for reporting this to the Numba issue tracker. I can confirm that I am able to reproduce this behaviour on `master`.
Looking into this some more, it seems like `a` is indeed of type `uint8`:

<img width="892" alt="Screen Shot 2021-07-30 at 16 04 43" src="https://user-images.githubusercontent.com/41563/127664814-00e4de4e-08c1-40f6-9808-dcb61eb5ecfa.png">

So my hunch is that the resulting `uint8` is incorrectly casted somewhere in the `getitem` call.

So, after some more debugging it turns out, that the index is first handled (i.e. clamped and converted from negative) and THEN cast to the correct type. This may not be the best order. Perhaps it is better to cast first and then handle. The handler obviously assumes signed integers. 

I have created the following the starter patch which does make it so the first example returns (128, 1, 128).

```
diff --git i/numba/typed/listobject.py w/numba/typed/listobject.py
index 77021cb694..7b38364413 100644
--- i/numba/typed/listobject.py
+++ w/numba/typed/listobject.py
@@ -764,9 +764,9 @@ def impl_getitem(l, index):
     if index in index_types:
         if IS_NOT_NONE:
             def integer_non_none_impl(l, index):
-                index = handle_index(l, index)
                 castedindex = _cast(index, indexty)
-                status, item = _list_getitem(l, castedindex)
+                handledindex = handle_index(l, castedindex)
+                status, item = _list_getitem(l, handledindex)
                 if status == ListStatus.LIST_OK:
                     return _nonoptional(item)
                 else:
```
@LunarLanding a potential fix is now available at: https://github.com/numba/numba/pull/7262
I am picking up work on this issue again. A simplified reproducer is available at:

```
import numba as nb


@nb.njit
def f():
    l = nb.typed.List.empty_list(nb.uint8)
    for i in range(129):
        l.append(nb.uint8(i))
    a = nb.uint8(128)
    return l[a],l[nb.int8(a)],l[nb.uint16(a)]


print(f())
```



Patch:
diff --git a/numba/typed/listobject.py b/numba/typed/listobject.py
index 68aaa52530e..78290ebc512 100644
--- a/numba/typed/listobject.py
+++ b/numba/typed/listobject.py
@@ -764,9 +764,9 @@ def impl_getitem(l, index):
     if index in index_types:
         if IS_NOT_NONE:
             def integer_non_none_impl(l, index):
-                index = handle_index(l, index)
                 castedindex = _cast(index, indexty)
-                status, item = _list_getitem(l, castedindex)
+                handledindex = handle_index(l, castedindex)
+                status, item = _list_getitem(l, handledindex)
                 if status == ListStatus.LIST_OK:
                     return _nonoptional(item)
                 else:


Test patch:
diff --git a/numba/tests/test_listobject.py b/numba/tests/test_listobject.py
index b34e0f66709..1d3d8b3aa6c 100644
--- a/numba/tests/test_listobject.py
+++ b/numba/tests/test_listobject.py
@@ -223,12 +223,26 @@ def foo(i):
                   ):
             self.assertEqual(foo((t(0))), 0)
 
+    def test_list_getitem_different_sized_uint_index(self):
+        # Checks that the index type cast and ext/trunc to the
+        # type of the length is correct, both wraparound and
+        # direct index is tested via -1/0.
+
+        for ty in types.unsigned_domain:
+            @njit
+            def foo():
+                l = listobject.new_list(int32)
+                l.append(7)
+                return l[ty(0)]
+
+            self.assertEqual(foo(), 7)
+
     def test_list_getitem_different_sized_int_index(self):
         # Checks that the index type cast and ext/trunc to the
         # type of the length is correct, both wraparound and
         # direct index is tested via -1/0.
 
-        for ty in types.integer_domain:
+        for ty in types.signed_domain:
             @njit
             def foo():
                 l = listobject.new_list(int32)
diff --git a/numba/tests/test_typedlist.py b/numba/tests/test_typedlist.py
index 6a78a3bb82d..529c75711ed 100644
--- a/numba/tests/test_typedlist.py
+++ b/numba/tests/test_typedlist.py
@@ -7,7 +7,7 @@
 
 from numba import config
 from numba import njit
-from numba import int32, float32, prange
+from numba import int32, float32, prange, uint8
 from numba.core import types
 from numba import typeof
 from numba.typed import List, Dict
@@ -1441,6 +1441,22 @@ def bar():
                 )
 
 
+class TestGetItemIndexType(MemoryLeakMixin, TestCase):
+
+    def test_indexing_with_uint8(self):
+        """ Test for reproducer at https://github.com/numba/numba/issues/7250
+        """
+        @njit
+        def foo():
+            l = List.empty_list(uint8)
+            for i in range(129):
+                l.append(uint8(i))
+            a = uint8(128)
+            return l[a]
+
+        self.assertEqual(foo(), 128)
+
+
 class TestListFromIter(MemoryLeakMixin, TestCase):
 
     def test_simple_iterable_types(self):