Repository: nv-legate/cunumeric

Issue:
reshape doesn't match numpy semantics
```
>>> import numpy
>>> import cunumeric
>>> x = cunumeric.ones((25))
>>> x.reshape(-1, 5)
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
       1., 1., 1., 1., 1., 1., 1., 1.])
>>> numpy.reshape(x, (-1, 5))
array([[1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1.]])
>>>
```


Discussion:

Resolving patch:
diff --git a/cunumeric/array.py b/cunumeric/array.py
--- a/cunumeric/array.py
+++ b/cunumeric/array.py
@@ -16,7 +16,6 @@
 
 import operator
 import warnings
-from collections.abc import Iterable
 from functools import reduce, wraps
 from inspect import signature
 from typing import Any, Callable, Optional, Set, TypeVar, Union
@@ -2978,7 +2977,7 @@ def ravel(self, order="C") -> ndarray:
         """
         return self.reshape(-1, order=order)
 
-    def reshape(self, shape, order="C") -> ndarray:
+    def reshape(self, *args, order="C") -> ndarray:
         """a.reshape(shape, order='C')
 
         Returns an array containing the same data with a new shape.
@@ -2994,61 +2993,51 @@ def reshape(self, shape, order="C") -> ndarray:
         --------
         Multiple GPUs, Multiple CPUs
         """
-        if shape != -1:
-            # Check that these sizes are compatible
-            if isinstance(shape, Iterable):
-                newsize = 1
-                newshape = list()
-                unknown_axis = -1
-                for ax, dim in enumerate(shape):
-                    if dim < 0:
-                        newshape.append(np.newaxis)
-                        if unknown_axis == -1:
-                            unknown_axis = ax
-                        else:
-                            unknown_axis = -2
-                    else:
-                        newsize *= dim
-                        newshape.append(dim)
-                if unknown_axis == -2:
-                    raise ValueError("can only specify one unknown dimension")
-                if unknown_axis >= 0:
-                    if self.size % newsize != 0:
-                        raise ValueError(
-                            "cannot reshape array of size "
-                            + str(self.size)
-                            + " into shape "
-                            + str(tuple(newshape))
-                        )
-                    newshape[unknown_axis] = self.size // newsize
-                    newsize *= newshape[unknown_axis]
-                if newsize != self.size:
-                    raise ValueError(
-                        "cannot reshape array of size "
-                        + str(self.size)
-                        + " into shape "
-                        + str(shape)
-                    )
-                shape = tuple(newshape)
-            elif isinstance(shape, int):
-                if shape != self.size:
-                    raise ValueError(
-                        "cannot reshape array of size "
-                        + str(self.size)
-                        + " into shape "
-                        + str((shape,))
-                    )
-            else:
-                TypeError("shape must be int-like or tuple-like")
+
+        if len(args) == 0:
+            raise TypeError("reshape() takes exactly 1 argument (0 given)")
+        elif len(args) == 1:
+            shape = (args[0],) if isinstance(args[0], int) else args[0]
         else:
-            # Compute a flattened version of the shape
-            shape = (self.size,)
+            shape = args
+
+        computed_shape = tuple(operator.index(extent) for extent in shape)
+
+        num_unknowns = sum(extent < 0 for extent in computed_shape)
+        if num_unknowns > 1:
+            raise ValueError("can only specify one unknown dimension")
+
+        knowns = filter(lambda x: x >= 0, computed_shape)
+        known_volume = reduce(lambda x, y: x * y, knowns, 1)
+
+        # Can't have an unknown if the known shape has 0 size
+        if num_unknowns > 0 and known_volume == 0:
+            raise ValueError(
+                f"cannot reshape array of size {self.size} into "
+                f"shape {computed_shape}"
+            )
+
+        size = self.size
+        unknown_extent = 1 if num_unknowns == 0 else size // known_volume
+
+        if unknown_extent * known_volume != size:
+            raise ValueError(
+                f"cannot reshape array of size {size} into "
+                f"shape {computed_shape}"
+            )
+
+        computed_shape = tuple(
+            unknown_extent if extent < 0 else extent
+            for extent in computed_shape
+        )
+
         # Handle an easy case
-        if shape == self.shape:
+        if computed_shape == self.shape:
             return self
+
         return ndarray(
             shape=None,
-            thunk=self._thunk.reshape(shape, order),
+            thunk=self._thunk.reshape(computed_shape, order),
         )
 
     def setfield(self, val, dtype, offset=0):


Test patch:
diff --git a/tests/integration/test_reshape.py b/tests/integration/test_reshape.py
--- a/tests/integration/test_reshape.py
+++ b/tests/integration/test_reshape.py
@@ -20,6 +20,7 @@
 
 SQUARE_CASES = [
     (10, 5, 2),
+    (-1, 5, 2),
     (5, 2, 10),
     (5, 2, 5, 2),
     (10, 10, 1),
@@ -33,26 +34,26 @@ class TestSquare:
     anp = np.arange(100).reshape(10, 10)
 
     def test_basic(self):
-        a = num.arange(100).reshape((10, 10))
+        a = num.arange(100).reshape(10, 10)
         assert np.array_equal(self.anp, a)
 
     @pytest.mark.parametrize("shape", SQUARE_CASES, ids=str)
     def test_shape(self, shape):
-        a = num.arange(100).reshape((10, 10))
+        a = num.arange(100).reshape(10, 10)
         assert np.array_equal(
             num.reshape(a, shape),
             np.reshape(self.anp, shape),
         )
 
     def test_1d(self):
-        a = num.arange(100).reshape((10, 10))
+        a = num.arange(100).reshape(10, 10)
         assert np.array_equal(
             num.reshape(a, (100,)),
             np.reshape(self.anp, (100,)),
         )
 
     def test_ravel(self):
-        a = num.arange(100).reshape((10, 10))
+        a = num.arange(100).reshape(10, 10)
         assert np.array_equal(
             num.ravel(a),
             np.ravel(self.anp),
@@ -62,6 +63,7 @@ def test_ravel(self):
 RECT_CASES = [
     (10, 2, 10),
     (20, 10),
+    (20, -5),
     (5, 40),
     (200, 1),
     (1, 200),