Repository: numba/numba

Issue:
Record staticgetitem with integer index not implemented.
## Reporting a bug

- [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'.

Context: https://numba.discourse.group/t/accessing-structured-array-scalars-by-index/446

This ought to work:
```python
from numba import njit
import numpy as np
arr = np.array([(1, 2., 3.)], dtype='i, f, f')

@njit
def get_elem(arr, idx):
    return arr[idx][0] # record index is const 0

print(get_elem(arr, 0))
```
but doesn't as it is not implemented.

This does work:
```python
from numba import njit
import numpy as np
arr = np.array([(1, 2., 3.)], dtype='i, f, f')

@njit
def get_elem(arr, idx):
    return arr[idx]['f0'] # record index is const 'f0'

print(get_elem(arr, 0))
```
seems strangely inconsistent!


Discussion:

Resolving patch:
diff --git a/numba/core/typing/arraydecl.py b/numba/core/typing/arraydecl.py
--- a/numba/core/typing/arraydecl.py
+++ b/numba/core/typing/arraydecl.py
@@ -555,13 +555,22 @@ class StaticGetItemLiteralRecord(AbstractTemplate):
     def generic(self, args, kws):
         # Resolution of members for records
         record, idx = args
-        if isinstance(record, types.Record) and isinstance(idx, types.StringLiteral):
-            if idx.literal_value not in record.fields:
-                raise KeyError(f"Field '{idx.literal_value}' was not found in record with "
-                               f"fields {tuple(record.fields.keys())}")
-            ret = record.typeof(idx.literal_value)
-            assert ret
-            return signature(ret, *args)
+        if isinstance(record, types.Record):
+            if isinstance(idx, types.StringLiteral):
+                if idx.literal_value not in record.fields:
+                    raise KeyError(f"Field '{idx.literal_value}' was not found in record with "
+                                   f"fields {tuple(record.fields.keys())}")
+                ret = record.typeof(idx.literal_value)
+                assert ret
+                return signature(ret, *args)
+            elif isinstance(idx, types.IntegerLiteral):
+                if idx.literal_value >= len(record.fields):
+                    msg = f"Requested index {idx.literal_value} is out of range"
+                    raise IndexError(msg)
+                field_names = list(record.fields)
+                ret = record.typeof(field_names[idx.literal_value])
+                assert ret
+                return signature(ret, *args)
 
 
 @infer
@@ -571,10 +580,21 @@ class StaticSetItemRecord(AbstractTemplate):
     def generic(self, args, kws):
         # Resolution of members for record and structured arrays
         record, idx, value = args
-        if isinstance(record, types.Record) and isinstance(idx, str):
-            expectedty = record.typeof(idx)
-            if self.context.can_convert(value, expectedty) is not None:
-                return signature(types.void, record, types.literal(idx), value)
+        if isinstance(record, types.Record):
+            if isinstance(idx, str):
+                expectedty = record.typeof(idx)
+                if self.context.can_convert(value, expectedty) is not None:
+                    return signature(types.void, record, types.literal(idx),
+                                     value)
+            elif isinstance(idx, int):
+                if idx >= len(record.fields):
+                    msg = f"Requested index {idx} is out of range"
+                    raise IndexError(msg)
+                str_field = list(record.fields)[idx]
+                expectedty = record.typeof(str_field)
+                if self.context.can_convert(value, expectedty) is not None:
+                    return signature(types.void, record, types.literal(idx),
+                                     value)
 
 
 @infer_global(operator.setitem)
diff --git a/numba/np/arrayobj.py b/numba/np/arrayobj.py
--- a/numba/np/arrayobj.py
+++ b/numba/np/arrayobj.py
@@ -2444,7 +2444,7 @@ def record_setattr(context, builder, sig, args, attr):
 
 
 @lower_builtin('static_getitem', types.Record, types.StringLiteral)
-def record_getitem(context, builder, sig, args):
+def record_static_getitem_str(context, builder, sig, args):
     """
     Record.__getitem__ redirects to getattr()
     """
@@ -2452,8 +2452,20 @@ def record_getitem(context, builder, sig, args):
     return impl(context, builder, sig.args[0], args[0], args[1])
 
 
+@lower_builtin('static_getitem', types.Record, types.IntegerLiteral)
+def record_static_getitem_int(context, builder, sig, args):
+    """
+    Record.__getitem__ redirects to getattr()
+    """
+    idx = sig.args[1].literal_value
+    fields = list(sig.args[0].fields)
+    ll_field = context.insert_const_string(builder.module, fields[idx])
+    impl = context.get_getattr(sig.args[0], ll_field)
+    return impl(context, builder, sig.args[0], args[0], fields[idx])
+
+
 @lower_builtin('static_setitem', types.Record, types.StringLiteral, types.Any)
-def record_setitem(context, builder, sig, args):
+def record_static_setitem_str(context, builder, sig, args):
     """
     Record.__setitem__ redirects to setattr()
     """
@@ -2465,6 +2477,20 @@ def record_setitem(context, builder, sig, args):
     return impl(builder, (rec, val))
 
 
+@lower_builtin('static_setitem', types.Record, types.IntegerLiteral, types.Any)
+def record_static_setitem_int(context, builder, sig, args):
+    """
+    Record.__setitem__ redirects to setattr()
+    """
+    recty, _, valty = sig.args
+    rec, idx, val = args
+    getattr_sig = signature(sig.return_type, recty, valty)
+    fields = list(sig.args[0].fields)
+    impl = context.get_setattr(fields[idx], getattr_sig)
+    assert impl is not None
+    return impl(builder, (rec, val))
+
+
 # ------------------------------------------------------------------------------
 # Constant arrays and records
 


Test patch:
diff --git a/numba/tests/test_record_dtype.py b/numba/tests/test_record_dtype.py
--- a/numba/tests/test_record_dtype.py
+++ b/numba/tests/test_record_dtype.py
@@ -43,6 +43,10 @@ def get_zero_a(ary, _unused):
 getitem_a = make_getitem('a')
 getitem_b = make_getitem('b')
 getitem_c = make_getitem('c')
+getitem_0 = make_getitem(0)
+getitem_1 = make_getitem(1)
+getitem_2 = make_getitem(2)
+getitem_10 = make_getitem(10)  # OOB
 
 
 def get_a_subarray(ary, i):
@@ -110,6 +114,10 @@ def set_xx(ary, i, v):
 setitem_a = make_setitem('a')
 setitem_b = make_setitem('b')
 setitem_c = make_setitem('c')
+setitem_0 = make_setitem(0)
+setitem_1 = make_setitem(1)
+setitem_2 = make_setitem(2)
+setitem_10 = make_setitem(10)  # OOB
 
 
 def set_a_subarray(ary, i, v):
@@ -509,6 +517,19 @@ def test_get_c(self):
         self._test_get_equal(getitem_c)
         self._test_get_equal(getitem_c_subarray)
 
+    def test_getitem_static_int_index(self):
+        self._test_get_equal(getitem_0)
+        self._test_get_equal(getitem_1)
+        self._test_get_equal(getitem_2)
+
+        # this exception on accessing OOB integer index
+        rec = numpy_support.from_dtype(recordtype)
+        with self.assertRaises(TypingError) as raises:
+            self.get_cfunc(getitem_10, (rec[:], types.intp))
+
+        msg = "Requested index 10 is out of range"
+        self.assertIn(msg, str(raises.exception))
+
     def _test_get_two_equal(self, pyfunc):
         '''
         Test with two arrays of the same type
@@ -585,6 +606,24 @@ def check(pyfunc):
         check(setitem_c)
         check(setitem_c_subarray)
 
+    def test_setitem_static_int_index(self):
+        def check(pyfunc):
+            self._test_set_equal(pyfunc, 3.1415, types.float64)
+            # Test again to check if coercion works
+            self._test_set_equal(pyfunc, 3., types.float32)
+
+        check(setitem_0)
+        check(setitem_1)
+        check(setitem_2)
+
+        # this exception on accessing OOB integer index
+        rec = numpy_support.from_dtype(recordtype)
+        with self.assertRaises(TypingError) as raises:
+            self.get_cfunc(setitem_10, (rec[:], types.intp, types.float64))
+
+        msg = "Requested index 10 is out of range"
+        self.assertIn(msg, str(raises.exception))
+
     def test_set_record(self):
         pyfunc = set_record
         rec = numpy_support.from_dtype(recordtype)