Repository: cython/cython

Issue:
[BUG] segfault on pandas with 3.0a9
Building pandas master (which works with cython 0.29.x) with the 3.0a9 I'm getting a segfault in https://github.com/pandas-dev/pandas/blob/master/pandas/_libs/hashtable_func_helper.pxi.in#L110

lldb is pointing me towards a `__Pyx_INCREF` call:

```
frame #1: 0x000000011da6ef72 hashtable.cpython-39-darwin.so`__pyx_pw_6pandas_5_libs_9hashtable_73__pyx_fuse_12duplicated [inlined] __pyx_f_6pandas_5_libs_9hashtable_duplicated_object(__pyx_v_values=0x0000000000000000, __pyx_optional_args=<unavailable>) at hashtable.c:101383 [opt]
   101380	 *         raise ValueError('keep must be either "first", "last" or False')
   101381	 * 
   101382	 */
-> 101383	  __Pyx_INCREF(__pyx_v_keep);
   101384	  __pyx_t_2 = __pyx_v_keep;
   101385	  __pyx_t_9 = (__Pyx_PyUnicode_Equals(__pyx_t_2, __pyx_n_u_last, Py_NE)); if (unlikely((__pyx_t_9 < 0))) __PYX_ERR(3, 1504, __pyx_L1_error)
   101386	  if (__pyx_t_9) {
```

Not sure if this means anything, but sublimetext's syntax highlighting seems to stop about 60k lines before this


Discussion:
It looks from that traceback as if `values` is being passed a NULL pointer (which is probably a Cython bug, but likely happening a little further up the traceback).

Could you tell us what code you're running to get it to crash?
`pytest pandas/tests/libs/`
@jbrockmendel do you know which case is crashing here? Is the caller passing `keep` in or is it using the default value?

(Note that it's not `values` being passed as `NULL` but the `keep` argument.)
It's the object-dtype `duplicated` case.  It segfaults with or without passing `keep`:

```
import numpy as np
from pandas._libs import hashtable as ht

arr = np.arange(100).astype(object)

ht.duplicated(arr)  # <- segfault
ht.duplicated(arr, "first") # <- segfault
ht.duplicated(arr, keep="first") # <- segfault
```
The following Pandas-free example reproduces the bug:

```
ctypedef fused F:
    double
    int

cpdef duplicated(F val, object keep="first"):
    if keep not in ('last', 'first', False):
        raise ValueError('keep must be either "first", "last" or False')
```

It's to do with default values on `cpdef` fused functions
The commit at issue looks to be ee7ae0b9ce0dea51cf17adf119afe439917ee4c0 - I suspect it just needs a little special-casing for cpdef functions

Resolving patch:
diff --git a/Cython/Compiler/ExprNodes.py b/Cython/Compiler/ExprNodes.py
--- a/Cython/Compiler/ExprNodes.py
+++ b/Cython/Compiler/ExprNodes.py
@@ -13814,6 +13814,12 @@ def generate_result_code(self, code):
     def generate_disposal_code(self, code):
         pass
 
+    def generate_post_assignment_code(self, code):
+        # if we're assigning from a CloneNode then it's "giveref"ed away, so it does
+        # need a matching incref (ideally this should happen before the assignment though)
+        if self.is_temp:  # should usually be true
+            code.put_incref(self.result(), self.ctype())
+
     def free_temps(self, code):
         pass
 
diff --git a/Cython/Compiler/FusedNode.py b/Cython/Compiler/FusedNode.py
--- a/Cython/Compiler/FusedNode.py
+++ b/Cython/Compiler/FusedNode.py
@@ -841,7 +841,8 @@ def analyse_expressions(self, env):
         for arg in self.node.args:
             if arg.default:
                 arg.default = arg.default.analyse_expressions(env)
-                defaults.append(ProxyNode(arg.default))
+                # coerce the argument to temp since CloneNode really requires a temp
+                defaults.append(ProxyNode(arg.default.coerce_to_temp(env)))
             else:
                 defaults.append(None)
 
@@ -851,7 +852,7 @@ def analyse_expressions(self, env):
                 # the dispatcher specifically doesn't want its defaults overriding
                 for arg, default in zip(stat.args, defaults):
                     if default is not None:
-                        arg.default = CloneNode(default).coerce_to(arg.type, env)
+                        arg.default = CloneNode(default).analyse_expressions(env).coerce_to(arg.type, env)
 
         if self.py_func:
             args = [CloneNode(default) for default in defaults if default]
diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -2348,9 +2348,11 @@ def generate_wrapper_functions(self, code):
     def generate_execution_code(self, code):
         code.mark_pos(self.pos)
         # Evaluate and store argument default values
-        for arg in self.args:
-            if not arg.is_dynamic:
-                arg.generate_assignment_code(code)
+        # skip this for wrappers since it's done by wrapped function
+        if not self.is_wrapper:
+            for arg in self.args:
+                if not arg.is_dynamic:
+                    arg.generate_assignment_code(code)
 
     #
     # Special code for the __getbuffer__ function


Test patch:
diff --git a/tests/run/fused_cpdef.pyx b/tests/run/fused_cpdef.pyx
--- a/tests/run/fused_cpdef.pyx
+++ b/tests/run/fused_cpdef.pyx
@@ -1,4 +1,4 @@
-# cython: language_level=3
+# cython: language_level=3str
 # mode: run
 
 cimport cython
@@ -186,3 +186,26 @@ def test_ambiguousmatch():
     Traceback (most recent call last):
     TypeError: Function call with ambiguous argument types
     """
+
+# https://github.com/cython/cython/issues/4409
+# default arguments + fused cpdef were crashing
+cpdef literal_default(cython.integral x, some_string="value"):
+    return x, some_string
+
+cpdef mutable_default(cython.integral x, some_value=[]):
+    some_value.append(x)
+    return some_value
+
+def test_defaults():
+    """
+    >>> literal_default(1)
+    (1, 'value')
+    >>> literal_default(1, "hello")
+    (1, 'hello')
+    >>> mutable_default(1)
+    [1]
+    >>> mutable_default(2)
+    [1, 2]
+    >>> mutable_default(3,[])
+    [3]
+    """