Repository: numba/numba

Issue:
Inconsistent behavior of reshape between numpy and numba/cuda device array
Consider the below code. Numpy will happily reshape using -1 to fill in any missing dimension, numba/cuda will not. Please mirror numpy behavior.
```
import numpy as np
import numba as nb
from numba import cuda
sixty_four = np.zeros(64)

blocks_of_thirty_two = sixty_four.reshape(-1,32)
nb_sixty_four = cuda.to_device(sixty_four)
nb_sixty_four.reshape(-1, 32)
```


Discussion:
Thanks for the report, I can reproduce. This seems to simply be not implemented but should be relatively easy to do. A branch needs adding here (referenced below) to check for a single `-1` instance in the `newdims` and then the computation doing to work out the missing dimension and then an update to `newdims` to reflect this.
https://github.com/numba/numba/blob/12355c16ef70d10b0812c4266a3e143bd135a7f1/numba/misc/dummyarray.py#L316-L323
I'd love to contribute to Numba and take this on as a good first issue! Found the numpy implementation of computing the missing dimension [here](https://github.com/numpy/numpy/blob/623bc1fae1d47df24e7f1e29321d0c0ba2771ce0/numpy/core/src/multiarray/shape.c#L470-L515) and am replicating similar logic in the section of `dummyarray.py` referenced by @stuartarchibald.  
> I'd love to contribute to Numba and take this on as a good first issue! Found the numpy implementation of computing the missing dimension [here](https://github.com/numpy/numpy/blob/623bc1fae1d47df24e7f1e29321d0c0ba2771ce0/numpy/core/src/multiarray/shape.c#L470-L515) and am replicating similar logic in the section of `dummyarray.py` referenced by @stuartarchibald.

Great, thanks @laurenarnett !

As a new contributor, some instructions for getting set up for development are [here](http://numba.pydata.org/numba-doc/latest/developer/contributing.html#getting-set-up) and are a great place to start.

Resolving patch:
diff --git a/numba/misc/dummyarray.py b/numba/misc/dummyarray.py
--- a/numba/misc/dummyarray.py
+++ b/numba/misc/dummyarray.py
@@ -314,6 +314,28 @@ def reshape(self, *newdims, **kws):
         if order not in 'CFA':
             raise ValueError('order not C|F|A')
 
+        # check for exactly one instance of -1 in newdims
+        # https://github.com/numpy/numpy/blob/623bc1fae1d47df24e7f1e29321d0c0ba2771ce0/numpy/core/src/multiarray/shape.c#L470-L515   # noqa: E501
+        unknownidx = -1
+        knownsize = 1
+        for i, dim in enumerate(newdims):
+            if dim < 0:
+                if unknownidx == -1:
+                    unknownidx = i
+                else:
+                    raise ValueError("can only specify one unknown dimension")
+            else:
+                knownsize *= dim
+
+        # compute the missing dimension
+        if unknownidx >= 0:
+            if knownsize == 0 or self.size % knownsize != 0:
+                raise ValueError("cannot infer valid shape for unknown dimension")
+            else:
+                newdims = newdims[0:unknownidx] \
+                        + (self.size // knownsize,) \
+                        + newdims[unknownidx + 1:]
+
         newsize = functools.reduce(operator.mul, newdims, 1)
 
         if order == 'A':


Test patch:
diff --git a/numba/tests/test_dummyarray.py b/numba/tests/test_dummyarray.py
--- a/numba/tests/test_dummyarray.py
+++ b/numba/tests/test_dummyarray.py
@@ -208,6 +208,69 @@ def test_reshape_3d1d(self):
         self.assertEqual(got.shape, expect.shape)
         self.assertEqual(got.strides, expect.strides)
 
+    def test_reshape_infer2d2d(self):
+        nparr = np.empty((4, 5))
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+        expect = nparr.reshape(-1, 4)
+        got = arr.reshape(-1, 4)[0]
+        self.assertEqual(got.shape, expect.shape)
+        self.assertEqual(got.strides, expect.strides)
+
+    def test_reshape_infer2d1d(self):
+        nparr = np.empty((4, 5))
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+        expect = nparr.reshape(-1)
+        got = arr.reshape(-1)[0]
+        self.assertEqual(got.shape, expect.shape)
+        self.assertEqual(got.strides, expect.strides)
+
+    def test_reshape_infer3d3d(self):
+        nparr = np.empty((3, 4, 5))
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+        expect = nparr.reshape(5, -1, 4)
+        got = arr.reshape(5, -1, 4)[0]
+        self.assertEqual(got.shape, expect.shape)
+        self.assertEqual(got.strides, expect.strides)
+
+    def test_reshape_infer3d2d(self):
+        nparr = np.empty((3, 4, 5))
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+        expect = nparr.reshape(3, -1)
+        got = arr.reshape(3, -1)[0]
+        self.assertEqual(got.shape, expect.shape)
+        self.assertEqual(got.strides, expect.strides)
+
+    def test_reshape_infer3d1d(self):
+        nparr = np.empty((3, 4, 5))
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+        expect = nparr.reshape(-1)
+        got = arr.reshape(-1)[0]
+        self.assertEqual(got.shape, expect.shape)
+        self.assertEqual(got.strides, expect.strides)
+
+    def test_reshape_infer_two_unknowns(self):
+        nparr = np.empty((3, 4, 5))
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+
+        with self.assertRaises(ValueError) as raises:
+            arr.reshape(-1, -1, 3)
+        self.assertIn('can only specify one unknown dimension', str(raises.exception))
+
+    def test_reshape_infer_invalid_shape(self):
+        nparr = np.empty((3, 4, 5))
+        arr = Array.from_desc(0, nparr.shape, nparr.strides,
+                              nparr.dtype.itemsize)
+
+        with self.assertRaises(ValueError) as raises:
+            arr.reshape(-1, 7)
+        self.assertIn('cannot infer valid shape for unknown dimension', str(raises.exception))
+
 
 class TestSqueeze(unittest.TestCase):
     def test_squeeze(self):