Repository: numba/numba

Issue:
Array to Tuple conversion
We should provide a way to convert an array into a tuple without type hackery.

Hackery:

```python

        @cuda.jit('UniTuple(i8, %d)(i8[:])' % lhs.ndim, device=True)
        def cast(it):
            """
            We can't dynamically look up an element in lhs with an array
            due to `FancyIndexer`: it interprets `a[[1, 2]] = b` as
            `a[1, :] = b` and `a[2, :] = b`. To generate the right code we need
            to pass in a `BaseTuple` as the index -- so cast it here.
            """
            return it
```
(from https://github.com/numba/numba/pull/2681/)

Array to Tuple conversion
We should provide a way to convert an array into a tuple without type hackery.

Hackery:

```python

        @cuda.jit('UniTuple(i8, %d)(i8[:])' % lhs.ndim, device=True)
        def cast(it):
            """
            We can't dynamically look up an element in lhs with an array
            due to `FancyIndexer`: it interprets `a[[1, 2]] = b` as
            `a[1, :] = b` and `a[2, :] = b`. To generate the right code we need
            to pass in a `BaseTuple` as the index -- so cast it here.
            """
            return it
```
(from https://github.com/numba/numba/pull/2681/)



Discussion:

Resolving patch:
diff --git a/numba/unsafe/ndarray.py b/numba/unsafe/ndarray.py
--- a/numba/unsafe/ndarray.py
+++ b/numba/unsafe/ndarray.py
@@ -2,13 +2,14 @@
 This file provides internal compiler utilities that support certain special
 operations with numpy.
 """
-import numpy as np
-
 from numba import types
+from numba import typing
 from numba.cgutils import unpack_tuple
 from numba.extending import intrinsic
 from numba.targets.imputils import impl_ret_new_ref
+from numba.errors import RequireConstValue, TypingError
 
+from .tuple import tuple_setitem
 
 
 @intrinsic
@@ -36,3 +37,45 @@ def codegen(context, builder, signature, args):
     array_ty = types.Array(ndim=nd, layout='C', dtype=types.undefined)
     sig = array_ty(shape)
     return sig, codegen
+
+
+@intrinsic(support_literals=True)
+def to_fixed_tuple(typingctx, array, length):
+    """Convert *array* into a tuple of *length*
+
+    Returns ``UniTuple(array.dtype, length)``
+
+    ** Warning **
+    - No boundchecking.
+      If *length* is longer than *array.size*, the behavior is undefined.
+    """
+    if not isinstance(length, types.Const):
+        raise RequireConstValue('*length* argument must be a constant')
+
+    if array.ndim != 1:
+        raise TypingError("Not supported on array.ndim={}".format(array.ndim))
+
+    # Determine types
+    tuple_size = int(length.value)
+    tuple_type = types.UniTuple(dtype=array.dtype, count=tuple_size)
+    sig = tuple_type(array, length)
+
+    def codegen(context, builder, signature, args):
+        def impl(array, length, empty_tuple):
+            out = empty_tuple
+            for i in range(length):
+                out = tuple_setitem(out, i, array[i])
+            return out
+
+        inner_argtypes = [signature.args[0], types.intp, tuple_type]
+        inner_sig = typing.signature(tuple_type, *inner_argtypes)
+        ll_idx_type = context.get_value_type(types.intp)
+        # Allocate an empty tuple
+        empty_tuple = context.get_constant_undef(tuple_type)
+        inner_args = [args[0], ll_idx_type(tuple_size), empty_tuple]
+
+        res = context.compile_internal(builder, impl, inner_sig, inner_args)
+        return res
+
+    return sig, codegen
+
diff --git a/numba/unsafe/tuple.py b/numba/unsafe/tuple.py
new file mode 100644
--- /dev/null
+++ b/numba/unsafe/tuple.py
@@ -0,0 +1,32 @@
+"""
+This file provides internal compiler utilities that support certain special
+operations with tuple and workarounds for limitations enforced in userland.
+"""
+
+from numba.cgutils import alloca_once
+from numba.extending import intrinsic
+
+
+@intrinsic
+def tuple_setitem(typingctx, tup, idx, val):
+    """Return a copy of the tuple with item at *idx* replaced with *val*.
+
+    Operation: ``out = tup[:idx] + (val,) + tup[idx + 1:]
+
+    **Warning**
+
+    - No boundchecking.
+    - The dtype of the tuple cannot be changed.
+      *val* is always cast to the existing dtype of the tuple.
+    """
+    def codegen(context, builder, signature, args):
+        tup, idx, val = args
+        stack = alloca_once(builder, tup.type)
+        builder.store(tup, stack)
+        # Unsafe load on unchecked bounds.  Poison value maybe returned.
+        offptr = builder.gep(stack, [idx.type(0), idx], inbounds=True)
+        builder.store(val, offptr)
+        return builder.load(stack)
+
+    sig = tup(tup, idx, tup.dtype)
+    return sig, codegen


Test patch:
diff --git a/numba/tests/test_unsafe_intrinsics.py b/numba/tests/test_unsafe_intrinsics.py
new file mode 100644
--- /dev/null
+++ b/numba/tests/test_unsafe_intrinsics.py
@@ -0,0 +1,85 @@
+from __future__ import print_function
+
+import random
+
+import numpy as np
+
+from .support import TestCase
+from numba import njit
+from numba.unsafe.tuple import tuple_setitem
+from numba.unsafe.ndarray import to_fixed_tuple
+from numba.errors import TypingError, RequireConstValue
+
+
+class TestTupleIntrinsic(TestCase):
+    """Tests for numba.unsafe.tuple
+    """
+    def test_tuple_setitem(self):
+        @njit
+        def foo(tup, idxs, vals):
+            out_tup = tup
+            for i, v in zip(idxs, vals):
+                out_tup = tuple_setitem(out_tup, i, v)
+            return tup, out_tup
+
+        random.seed(123)
+        for _ in range(20):
+            # Random data
+            n = random.randint(1, 10)
+            tup = tuple([random.randint(0, n) for i in range(n)])
+            vals = tuple([random.randint(10, 20) for i in range(n)])
+            idxs = list(range(len(vals)))
+            random.shuffle(idxs)
+            idxs = tuple(idxs)
+            # Expect
+            expect_tup = tuple(tup)
+            expect_out = np.asarray(expect_tup)
+            expect_out[np.asarray(idxs)] = vals
+            # Got
+            got_tup, got_out = foo(tup, idxs, vals)
+            # Check
+            self.assertEqual(got_tup, expect_tup)
+            self.assertEqual(got_out, tuple(expect_out))
+
+
+class TestNdarrayIntrinsic(TestCase):
+    """Tests for numba.unsafe.ndarray
+    """
+    def test_to_fixed_tuple(self):
+        const = 3
+
+        @njit
+        def foo(array):
+            a = to_fixed_tuple(array, length=1)
+            b = to_fixed_tuple(array, 2)
+            c = to_fixed_tuple(array, const)
+            d = to_fixed_tuple(array, 0)
+            return a, b, c, d
+
+        np.random.seed(123)
+        for _ in range(10):
+            # Random data
+            arr = np.random.random(3)
+            # Run
+            a, b, c, d = foo(arr)
+            # Check
+            self.assertEqual(a, tuple(arr[:1]))
+            self.assertEqual(b, tuple(arr[:2]))
+            self.assertEqual(c, tuple(arr[:3]))
+            self.assertEqual(d, ())
+
+        # Check error with ndim!=1
+        with self.assertRaises(TypingError) as raises:
+            foo(np.random.random((1, 2)))
+        self.assertIn("Not supported on array.ndim=2",
+                      str(raises.exception))
+
+        # Check error with non-constant length
+        @njit
+        def tuple_with_length(array, length):
+            return to_fixed_tuple(array, length)
+
+        with self.assertRaises(RequireConstValue) as raises:
+            tuple_with_length(np.random.random(3), 1)
+        self.assertIn("*length* argument must be a constant",
+                      str(raises.exception))