Repository: cython/cython

Issue:
[BUG] Taking a pointer to a const memoryview loses the constness
### Describe the bug

It's possible to get a non-const pointer from a const memoryview with no warnings and no casts

### Code to reproduce the behaviour:

```cython
def f(const double[::1] x):
    cdef double *xptr = &x[0]
```


### Expected behaviour

Constness should be preserved.

I think we probably have to make this a warning rather than an error - there's almost certain code depending on it

### OS

Linux

### Python version

3.11.4

### Cython version

3.0.0 ish

### Additional context

https://github.com/scikit-learn/scikit-learn/pull/27125

You do get a C warning (with GCC) but not a Cython warning


Discussion:

Resolving patch:
diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -6018,8 +6018,19 @@ class AssignmentNode(StatNode):
     #  parallel assignment to be evaluated before assigning
     #  to any of the left hand sides.
 
+    def _warn_on_const_assignment(self, lhs, rhs):
+        rhs_t = rhs.type
+        lhs_t = lhs.type
+        if rhs_t.is_ptr and rhs_t.base_type.is_const and lhs_t.is_ptr and not lhs_t.base_type.is_const:
+            warning(self.pos, "Assigning to '{}' from '{}' discards const qualifier".format(lhs_t, rhs_t), level=1)
+
+    def _check_const_assignment(self, node):
+        if isinstance(node, AssignmentNode):
+            self._warn_on_const_assignment(node.lhs, node.rhs)
+
     def analyse_expressions(self, env):
         node = self.analyse_types(env)
+        self._check_const_assignment(node)
         if isinstance(node, AssignmentNode) and not isinstance(node, ParallelAssignmentNode):
             if node.rhs.type.is_ptr and node.rhs.is_ephemeral():
                 error(self.pos, "Storing unsafe C derivative of temporary Python reference")
@@ -6370,6 +6381,11 @@ class CascadedAssignmentNode(AssignmentNode):
     coerced_values = None
     assignment_overloads = None
 
+    def _check_const_assignment(self, node):
+        if isinstance(node, CascadedAssignmentNode):
+            for lhs in node.lhs_list:
+                self._warn_on_const_assignment(lhs, node.rhs)
+
     def analyse_declarations(self, env):
         for lhs in self.lhs_list:
             lhs.analyse_target_declaration(env)
@@ -6476,6 +6492,9 @@ def analyse_declarations(self, env):
     def analyse_expressions(self, env):
         self.stats = [stat.analyse_types(env, use_temp=1)
                       for stat in self.stats]
+
+        for stat in self.stats:
+            stat._check_const_assignment(stat)
         return self
 
 #    def analyse_expressions(self, env):


Test patch:
diff --git a/tests/errors/const_decl_errors.pyx b/tests/errors/const_decl_errors.pyx
--- a/tests/errors/const_decl_errors.pyx
+++ b/tests/errors/const_decl_errors.pyx
@@ -1,5 +1,5 @@
 # mode: error
-
+# tag: warnings
 cdef const object o
 
 # TODO: This requires making the assignment at declaration time.
@@ -27,6 +27,19 @@ cdef func(const int a, const int* b, const (int*) c, const S s, int *const d, in
 
 cdef volatile object v
 
+cdef const_warn(const int *b, const int *c):
+    cdef int *x = b
+    cdef int *y
+    cdef int *z
+    y, z = b, c
+    z = y = b
+
+cdef const_ok(const int *b, const int *c):
+    cdef const int *x = b
+    cdef const int *y
+    cdef const int *z
+    y, z = b, c
+    z = y = b
 
 _ERRORS = """
 3:5: Const/volatile base type cannot be a Python object
@@ -41,3 +54,11 @@ _ERRORS = """
 26:4: Assignment to const 't'
 28:5: Const/volatile base type cannot be a Python object
 """
+
+_WARNINGS = """
+31:9: Assigning to 'int *' from 'const int *' discards const qualifier
+34:11: Assigning to 'int *' from 'const int *' discards const qualifier
+34:14: Assigning to 'int *' from 'const int *' discards const qualifier
+35:12: Assigning to 'int *' from 'const int *' discards const qualifier
+35:12: Assigning to 'int *' from 'const int *' discards const qualifier
+"""