Repository: cython/cython

Issue:
Error when using nogil in cpdef function that has nogil in signature
Code below used to compile in 0.29.21 but fails on master with error
>cpdef void test(const int i) nogil:
    with nogil, parallel(num_threads=i):
        ^
main.pyx:4:9: Trying to release the GIL while it was previously released. 

```
from cython.parallel import parallel

cpdef void test(const int i) nogil:
    with nogil, parallel(num_threads=i):
        pass
```
The [documentation](https://cython.readthedocs.io/en/latest/src/userguide/external_C_code.html#declaring-a-function-as-callable-without-the-gil) for `nogil` states:
>The function does not in itself release the GIL if it is held by the caller.

So the error looks like a bug.



Discussion:
That does look like a bug.

If you want to investigate, then `git bisect` is often useful for finding out why things have decided to stop working
Thinking about it more, I'm not sure if it's a bug. I think it depends on whether a `with nogil` block claims the GIL on exit, or if it restores the previous state. I don't know off the top my head.

If it claims the GIL on exit then the code was probably dodgy on Cython 0.29.

I'd it restores the previous state then there's no reason to make nested `with gil` blocks an error.

See also https://github.com/cython/cython/issues/3443
> If it restores the previous state then there's no reason to make nested `with gil` blocks an error.

Out of the top of my head, this is how it works. I agree that it should be allowed.

It looks like `with nogil` expands to:
```
#ifdef WITH_THREAD
PyThreadState *_save;
Py_UNBLOCK_THREADS  /* i.e. _save = PyEval_SaveThread() */
__Pyx_FastGIL_Remember();
#endif
```

then 

```
#ifdef WITH_THREAD
__Pyx_FastGIL_Forget();
Py_BLOCK_THREADS  /* PyEval_RestoreThread(_save); */
#endif
```

The documentation for `PyEval_SaveThread` says
>  If the lock has been created, the current thread must have acquired it.

and `PyEval_RestoreThread` says
> Acquire the global interpreter lock (if it has been created)

Therefore, the `with nogil` block can only be used when you genuinely hold the GIL and therefore the code that used to "work" in Cython 0.29 was broken.

Personally, I think it might be worth changing this since it's quite hard to do the right thing in a function marked `nogil` (where we don't actual know whether it has the GIL or not). `with gil` uses `PyGILState_Ensure/Release` which does allow recursive calls so should be safer (but doesn't work with subinterpretters)

Resolving patch:
diff --git a/Cython/Compiler/Code.py b/Cython/Compiler/Code.py
--- a/Cython/Compiler/Code.py
+++ b/Cython/Compiler/Code.py
@@ -2337,7 +2337,7 @@ def put_release_ensured_gil(self, variable=None):
         self.putln("__Pyx_PyGILState_Release(%s);" % variable)
         self.putln("#endif")
 
-    def put_acquire_gil(self, variable=None):
+    def put_acquire_gil(self, variable=None, unknown_gil_state=True):
         """
         Acquire the GIL. The thread's thread state must have been initialized
         by a previous `put_release_gil`
@@ -2347,15 +2347,26 @@ def put_acquire_gil(self, variable=None):
         self.putln("__Pyx_FastGIL_Forget();")
         if variable:
             self.putln('_save = %s;' % variable)
+        if unknown_gil_state:
+            self.putln("if (_save) {")
         self.putln("Py_BLOCK_THREADS")
+        if unknown_gil_state:
+            self.putln("}")
         self.putln("#endif")
 
-    def put_release_gil(self, variable=None):
+    def put_release_gil(self, variable=None, unknown_gil_state=True):
         "Release the GIL, corresponds to `put_acquire_gil`."
         self.use_fast_gil_utility_code()
         self.putln("#ifdef WITH_THREAD")
         self.putln("PyThreadState *_save;")
+        self.putln("_save = NULL;")
+        if unknown_gil_state:
+            # we don't *know* that we don't have the GIL (since we may be inside a nogil function,
+            # and Py_UNBLOCK_THREADS is unsafe without the GIL)
+            self.putln("if (PyGILState_Check()) {")
         self.putln("Py_UNBLOCK_THREADS")
+        if unknown_gil_state:
+            self.putln("}")
         if variable:
             self.putln('%s = _save;' % variable)
         self.putln("__Pyx_FastGIL_Remember();")
diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -8325,9 +8325,12 @@ class GILStatNode(NogilTryFinallyStatNode):
     #  'with gil' or 'with nogil' statement
     #
     #   state   string   'gil' or 'nogil'
+    #   scope_gil_state_known  bool  For nogil functions this can be False, since they can also be run with gil
+    #                           set to False by GilCheck transform
 
     child_attrs = ["condition"] + NogilTryFinallyStatNode.child_attrs
     state_temp = None
+    scope_gil_state_known = True
 
     def __init__(self, pos, state, body, condition=None):
         self.state = state
@@ -8390,7 +8393,7 @@ def generate_execution_code(self, code):
             code.put_ensure_gil(variable=variable)
             code.funcstate.gil_owned = True
         else:
-            code.put_release_gil(variable=variable)
+            code.put_release_gil(variable=variable, unknown_gil_state=not self.scope_gil_state_known)
             code.funcstate.gil_owned = False
 
         TryFinallyStatNode.generate_execution_code(self, code)
@@ -8407,10 +8410,13 @@ class GILExitNode(StatNode):
     Used as the 'finally' block in a GILStatNode
 
     state   string   'gil' or 'nogil'
+    #   scope_gil_state_known  bool  For nogil functions this can be False, since they can also be run with gil
+    #                           set to False by GilCheck transform
     """
 
     child_attrs = []
     state_temp = None
+    scope_gil_state_known = True
 
     def analyse_expressions(self, env):
         return self
@@ -8424,7 +8430,7 @@ def generate_execution_code(self, code):
         if self.state == 'gil':
             code.put_release_ensured_gil(variable)
         else:
-            code.put_acquire_gil(variable)
+            code.put_acquire_gil(variable, unknown_gil_state=not self.scope_gil_state_known)
 
 
 class EnsureGILNode(GILExitNode):
diff --git a/Cython/Compiler/ParseTreeTransforms.py b/Cython/Compiler/ParseTreeTransforms.py
--- a/Cython/Compiler/ParseTreeTransforms.py
+++ b/Cython/Compiler/ParseTreeTransforms.py
@@ -3052,6 +3052,7 @@ def visit_FuncDefNode(self, node):
         self.env_stack.append(node.local_scope)
         inner_nogil = node.local_scope.nogil
 
+        nogil_declarator_only = self.nogil_declarator_only
         if inner_nogil:
             self.nogil_declarator_only = True
 
@@ -3060,8 +3061,9 @@ def visit_FuncDefNode(self, node):
 
         self._visit_scoped_children(node, inner_nogil)
 
-        # This cannot be nested, so it doesn't need backup/restore
-        self.nogil_declarator_only = False
+        # FuncDefNodes can be nested, because a cpdef function contains a def function
+        # inside it. Therefore restore to previous state
+        self.nogil_declarator_only = nogil_declarator_only
 
         self.env_stack.pop()
         return node
@@ -3086,6 +3088,8 @@ def visit_GILStatNode(self, node):
             else:
                 error(node.pos, "Trying to release the GIL while it was "
                                 "previously released.")
+        if self.nogil_declarator_only:
+            node.scope_gil_state_known = False
 
         if isinstance(node.finally_clause, Nodes.StatListNode):
             # The finally clause of the GILStatNode is a GILExitNode,
@@ -3136,6 +3140,12 @@ def visit_TryFinallyStatNode(self, node):
         self.visitchildren(node)
         return node
 
+    def visit_GILExitNode(self, node):
+        if self.nogil_declarator_only:
+            node.scope_gil_state_known = False
+        self.visitchildren(node)
+        return node
+
     def visit_Node(self, node):
         if self.env_stack and self.nogil and node.nogil_check:
             node.nogil_check(self.env_stack[-1])


Test patch:
diff --git a/tests/run/nogil.pyx b/tests/run/nogil.pyx
--- a/tests/run/nogil.pyx
+++ b/tests/run/nogil.pyx
@@ -28,6 +28,46 @@ cdef int g(int x) nogil:
         y = x + 42
         return y
 
+cdef void release_gil_in_nogil() nogil:
+    # This should generate valid code with/without the GIL
+    with nogil:
+        pass
+
+cpdef void release_gil_in_nogil2() nogil:
+    # This should generate valid code with/without the GIL
+    with nogil:
+        pass
+
+def test_release_gil_in_nogil():
+    """
+    >>> test_release_gil_in_nogil()
+    """
+    with nogil:
+        release_gil_in_nogil()
+    with nogil:
+        release_gil_in_nogil2()
+    release_gil_in_nogil()
+    release_gil_in_nogil2()
+
+cdef void get_gil_in_nogil() nogil:
+    with gil:
+        pass
+
+cpdef void get_gil_in_nogil2() nogil:
+    with gil:
+        pass
+
+def test_get_gil_in_nogil():
+    """
+    >>> test_get_gil_in_nogil()
+    """
+    with nogil:
+        get_gil_in_nogil()
+    with nogil:
+        get_gil_in_nogil2()
+    get_gil_in_nogil()
+    get_gil_in_nogil2()
+
 cdef int with_gil_func() except -1 with gil:
     raise Exception("error!")