Repository: cython/cython
Pull request: always consider 0-sized arrays as C- and F-contiguous
fixes #2093

Problem statement:
ValueError continuous memory view and 0-size numpy array
A ValueError is raised if one gives a numpy array with shape `(2, 0, 1)` to such Cython functions with a 3d memoryview as argument:

```cython
cimport numpy as np
import numpy as np
np.import_array()

cpdef myfunc3d(np.float64_t[:, :, ::1] arr):
    if arr.size > 0:
        print(arr[0, 0, 0])
    else:
        print('size == 0')
```

The code to get the error:

```python
import numpy as np
arr = np.ones((2, 0, 1))
print(arr.flags)
myfunc3d(arr)
```
which gives:
```
C_CONTIGUOUS : True
  F_CONTIGUOUS : True
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  UPDATEIFCOPY : False
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-7-81a397764d36> in <module>()
      1 arr = np.ones((2, 0, 1))
      2 print(arr.flags)
----> 3 myfunc3d(arr)

_cython_magic_5dfab2d7f2a0e837d79c56566c9147b4.pyx in _cython_magic_5dfab2d7f2a0e837d79c56566c9147b4.myfunc3d()

ValueError: Buffer and memoryview are not contiguous in the same dimension.
In [ ]:
```
Strangely, no error is raised for a shape `(1, 0, 1)`!

Hints from issue discussion:
Strangely enough, by changing the function definition as shown below does not raise any error.

```python
from cython cimport view

def myfunc3d(np.float_t[:,:,::view.contiguous] arr):
```
Possible explanation from [Cython docs](https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html?highlight=view#specifying-more-general-memory-layouts)

> The difference between the contiguous flag and the ::1 specifier is that the former specifies contiguity for only one dimension, whereas the latter specifies contiguity for all following (Fortran) or preceding (C) dimensions:

However this is not a solution, the issue still persists, IMHO.
To add to the previous comment, ~~although no error is raised... the output `size == 0` is unexpected!~~
```python
>>> import numpy as np; arr = np.ascontiguousarray(np.ones((2, 0, 1))); print(arr.flags); myfunc3d(arr)

  C_CONTIGUOUS : True
  F_CONTIGUOUS : True
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False
size == 0

>>> arr.size

0
```
**EDIT**: Correction, I was not thinking clearly.
I ran into this too. Here's another minimal example to reproduce this:

```
cdef void foo(double[:, :, ::1] X):
    pass

import numpy as np
X = np.zeros((4,4,4))

# this works
foo(np.ascontiguousarray(X[:, 2:3, :]))

# this fails with 'ValueError: Buffer and memoryview are not contiguous in the
# same dimension.'
foo(np.ascontiguousarray(X[:, 2:2, :]))
```

The problem is that `__Pyx_ValidateAndInit_memviewslice` does not correctly realize that a 0-size array is **always** C-contiguous.

This crops up in the wild, too. A search for the error message reveals many instances where people inadvertently passed 0-size arrays to cython-implemented functions and got confusing error messages. E.g.: https://stackoverflow.com/questions/46470492/buffer-and-memoryview-are-not-contiguous-in-the-same-dimension-in-python
For reference, here is the corresponding check for contiguity in numpy:

https://github.com/numpy/numpy/blob/3873ec46816f3fe0f705394242f83a764d4ce90e/numpy/core/src/multiarray/flagsobject.c#L123

Note that it is contingent on the NPY_RELAXED_STRIDES_CHECKING compile-time environment variable, however this seems to be always on by default.

The Cython function `__Pyx_ValidateAndInit_memviewslice` does not have the same check for 0-length dimensions.
Yes, that's a bug. It should behave like NumPy.
PR welcome. Tests can go into `tests/memoryview/memslice.pyx`, there are several contiguity tests already.

Commit message: always consider 0-sized arrays as C- and F-contiguous

fixes #2093

Patch to review:
diff --git a/Cython/Utility/MemoryView_C.c b/Cython/Utility/MemoryView_C.c
index 063fbf3606b..dc34ef420c6 100644
--- a/Cython/Utility/MemoryView_C.c
+++ b/Cython/Utility/MemoryView_C.c
@@ -347,18 +347,23 @@ static int __Pyx_ValidateAndInit_memviewslice(
     }
 
     /* Check axes */
-    for (i = 0; i < ndim; i++) {
-        spec = axes_specs[i];
-        if (unlikely(!__pyx_check_strides(buf, i, ndim, spec)))
-            goto fail;
-        if (unlikely(!__pyx_check_suboffsets(buf, i, ndim, spec)))
+    if (buf->len > 0) {
+        /* 0-sized arrays do not undergo these checks since their strides are
+         * irrelevant and they are always both C- and F-contiguous
+         */
+        for (i = 0; i < ndim; i++) {
+            spec = axes_specs[i];
+            if (unlikely(!__pyx_check_strides(buf, i, ndim, spec)))
+                goto fail;
+            if (unlikely(!__pyx_check_suboffsets(buf, i, ndim, spec)))
+                goto fail;
+        }
+
+        /* Check contiguity */
+        if (unlikely(buf->strides && !__pyx_verify_contig(buf, ndim, c_or_f_flag)))
             goto fail;
     }
 
-    /* Check contiguity */
-    if (unlikely(buf->strides && !__pyx_verify_contig(buf, ndim, c_or_f_flag)))
-        goto fail;
-
     /* Initialize */
     if (unlikely(__Pyx_init_memviewslice(memview, ndim, memviewslice,
                                          new_memview != NULL) == -1)) {
diff --git a/tests/memoryview/relaxed_strides.pyx b/tests/memoryview/relaxed_strides.pyx
index f541a608a30..d8719e0b82a 100644
--- a/tests/memoryview/relaxed_strides.pyx
+++ b/tests/memoryview/relaxed_strides.pyx
@@ -62,3 +62,14 @@ def test_zero_sized(array):
     """
     cdef double[::1] a = array
     return a
+
+def test_zero_sized_multidim(array):
+    """
+    >>> contig = np.ascontiguousarray(np.zeros((4,4,4))[::2, 2:2, ::2])
+    >>> _ = test_zero_sized_multidim(contig)
+
+    >>> a = np.zeros((4,4,4))[::2, 2:2, ::2]
+    >>> if NUMPY_HAS_RELAXED_STRIDES: _ = test_zero_sized_multidim(a)
+    """
+    cdef double[:, :, ::1] a = array
+    return a


Code review comments:
diff_hunk: @@ -347,18 +347,23 @@ static int __Pyx_ValidateAndInit_memviewslice(
     }
 
     /* Check axes */
-    for (i = 0; i < ndim; i++) {
-        spec = axes_specs[i];
-        if (unlikely(!__pyx_check_strides(buf, i, ndim, spec)))
-            goto fail;
-        if (unlikely(!__pyx_check_suboffsets(buf, i, ndim, spec)))
+    if (buf->len > 0) {
+        /* 0-sized arrays do not undergo these checks since their strides are
+         * irrelevant and they are always both C- and F-contiguous
+         */
path: Cython/Utility/MemoryView_C.c
text: @user1:
C-style multi-line comments are not discarded from the generated C file.
```suggestion
        // 0-sized arrays do not undergo these checks since their strides are
        // irrelevant and they are always both C- and F-contiguous.
```