Repository: numba/numba

Issue:
NumbaTypeError: Cannot modify readonly array of type: readonly array(int64, 2d, C)
```
import numpy as np
from numba import njit

@njit
def unfold(x, y):
    r, c = x.shape
    a = np.broadcast_to(x, (y, r, c))
#     a = a.copy()
    b = np.swapaxes(a, 0, 1)
#     b = b.copy()
    cc = b.flatten()
#     c = c.copy()
    d = np.reshape(cc, (-1, c))
    # assign indicator to the right end of the period
    # in case of look forward bias
#     d = d.copy()
    d[y - 1 :, :] = d[: 1 - y]
    return d


unfold(np.array([[1, 1, 1], [2, 2, 2], [3,3,3]]), 2)
```
This will reproduce the error above. Under plain numpy this works fine.
By uncommenting any one of the copy() calls, the issue is resolved. So I have no idea which numpy call returns this readonly array.
Can it be implemented without an explicit copy call?




Discussion:
Thanks for the issue report and nice reproducer! I can reproduce the issue.

The first `readonly array` is `a`, so copying any of the subsequent values breaks the "chain" of readonly arrays.
Looks like `array.flatten` erroneously types its result as `readonly` if its input is `readonly`, which is unnecessary as it returns a copy. With:

```diff
diff --git a/numba/core/typing/arraydecl.py b/numba/core/typing/arraydecl.py
index 629263aad..edb4ca103 100644
--- a/numba/core/typing/arraydecl.py
+++ b/numba/core/typing/arraydecl.py
@@ -481,7 +481,7 @@ class ArrayAttribute(AttributeTemplate):
         # Only support no argument version (default order='C')
         assert not kws
         assert not args
-        return signature(ary.copy(ndim=1, layout='C'))
+        return signature(ary.copy(ndim=1, layout='C', readonly=False))
 
     @bound_function("array.take")
     def resolve_take(self, ary, args, kws):
```

the following:

```python
import numpy as np
from numba import njit


def unfold(x, y):
    r, c = x.shape
    a = np.broadcast_to(x, (y, r, c))
#     a = a.copy()
    b = np.swapaxes(a, 0, 1)
#     b = b.copy()
    cc = b.flatten()
#     c = c.copy()
    d = np.reshape(cc, (-1, c))
    # assign indicator to the right end of the period
    # in case of look forward bias
#     d = d.copy()
    d[y - 1:, :] = d[: 1 - y]
    return d


x = np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]])

jitted = njit(unfold)

print(unfold(x, 2))
print(jitted(x, 2))
```

gives

```
$ python repro.py 
[[1 1 1]
 [1 1 1]
 [1 1 1]
 [2 2 2]
 [2 2 2]
 [3 3 3]]
[[1 1 1]
 [1 1 1]
 [1 1 1]
 [2 2 2]
 [2 2 2]
 [3 3 3]]
```

as one would expect.

To fix this, the above change can be seen as a starter patch that needs test cases (probably including the reproducer from this report) adding. This may be a good first issue for a new contributor to take on, if a maintainer doesn't get round to this first, so I'll label it as such,
So you mean broadcast_to producing a readonly array is the expected behaviour, but under plain numpy even replacing `flatten` to `ravel` works which in the numpy document says `ravel` does not necessarily copy the array. So there could be some other unconsistency in behaviour of these calls between numba and numpy?

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
@@ -477,14 +477,22 @@ def resolve_ravel(self, ary, args, kws):
         # Only support no argument version (default order='C')
         assert not kws
         assert not args
-        return signature(ary.copy(ndim=1, layout='C'))
+        copy_will_be_made = ary.layout != 'C'
+        readonly = not (copy_will_be_made or ary.mutable)
+        return signature(ary.copy(ndim=1, layout='C', readonly=readonly))
 
     @bound_function("array.flatten")
     def resolve_flatten(self, ary, args, kws):
         # Only support no argument version (default order='C')
         assert not kws
         assert not args
-        return signature(ary.copy(ndim=1, layout='C'))
+        # To ensure that Numba behaves exactly like NumPy,
+        # we also clear the read-only flag when doing a "flatten"
+        # Why? Two reasons:
+        # Because flatten always returns a copy. (see NumPy docs for "flatten")
+        # And because a copy always returns a writeable array.
+        # ref: https://numpy.org/doc/stable/reference/generated/numpy.copy.html
+        return signature(ary.copy(ndim=1, layout='C', readonly=False))
 
     def generic_resolve(self, ary, attr):
         # Resolution of other attributes, for record arrays


Test patch:
diff --git a/numba/tests/test_array_manipulation.py b/numba/tests/test_array_manipulation.py
--- a/numba/tests/test_array_manipulation.py
+++ b/numba/tests/test_array_manipulation.py
@@ -1315,6 +1315,72 @@ def test_broadcast_arrays_incompatible_shapes_raise_valueerror(self):
                 self.assertIn("shape mismatch: objects cannot be broadcast to a single shape",
                               str(raises.exception))
 
+    def test_readonly_after_flatten(self):
+        # Reproduces Issue #8370
+        def unfold_flatten(x, y):
+            r, c = x.shape
+            a = np.broadcast_to(x, (y, r, c))
+            b = np.swapaxes(a, 0, 1)
+            cc = b.flatten()
+            d = np.reshape(cc, (-1, c))
+            d[y - 1:, :] = d[: 1 - y]
+            return d
+
+        pyfunc = unfold_flatten
+        cfunc = jit(nopython=True)(pyfunc)
+
+        # If issue #8370 is not fixed: This will fail.
+        res_nb = cfunc(np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]]), 2)
+        res_py = pyfunc(np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]]), 2)
+
+        np.testing.assert_array_equal(res_py, res_nb)
+
+    def test_readonly_after_ravel(self):
+        # Reproduces another suggested problem in Issue #8370
+        def unfold_ravel(x, y):
+            r, c = x.shape
+            a = np.broadcast_to(x, (y, r, c))
+            b = np.swapaxes(a, 0, 1)
+            cc = b.ravel()
+            d = np.reshape(cc, (-1, c))
+            d[y - 1:, :] = d[: 1 - y]
+            return d
+
+        pyfunc = unfold_ravel
+        cfunc = jit(nopython=True)(pyfunc)
+
+        # If issue #8370 is not fixed: This will fail.
+        res_nb = cfunc(np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]]), 2)
+        res_py = pyfunc(np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]]), 2)
+
+        np.testing.assert_array_equal(res_py, res_nb)
+
+    def test_mutability_after_ravel(self):
+        # Reproduces another suggested problem in Issue #8370
+        # Namely that ravel should only return a writable array
+        # if a copy took place... otherwise leave it as it is.
+        self.disable_leak_check()
+        a_c = np.arange(9).reshape((3, 3)).copy()
+        a_f = a_c.copy(order='F')
+        a_c.flags.writeable = False
+        a_f.flags.writeable = False
+
+        def try_ravel_w_copy(a):
+            result = a.ravel()
+            return result
+
+        pyfunc = try_ravel_w_copy
+        cfunc = jit(nopython=True)(pyfunc)
+
+        ret_c = cfunc(a_c)
+        ret_f = cfunc(a_f)
+
+        msg = 'No copy was performed, so the ' \
+              'resulting array must not be writeable'
+        self.assertTrue(not ret_c.flags.writeable, msg)
+
+        msg = 'A copy was performed, yet the resulting array is not modifiable'
+        self.assertTrue(ret_f.flags.writeable, msg)
 
 
 if __name__ == '__main__':