1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
# mode: run
cimport cython
# Count number of times an object was deallocated twice. This should remain 0.
cdef int double_deallocations = 0
def assert_no_double_deallocations():
global double_deallocations
err = double_deallocations
double_deallocations = 0
assert not err
# Compute x = f(f(f(...(None)...))) nested n times and throw away the result.
# The real test happens when exiting this function: then a big recursive
# deallocation of x happens. We are testing two things in the tests below:
# that Python does not crash and that no double deallocation happens.
# See also https://github.com/python/cpython/pull/11841
def recursion_test(f, int n=2**20):
x = None
cdef int i
for i in range(n):
x = f(x)
@cython.trashcan(True)
cdef class Recurse:
"""
>>> recursion_test(Recurse)
>>> assert_no_double_deallocations()
"""
cdef public attr
cdef int deallocated
def __cinit__(self, x):
self.attr = x
def __dealloc__(self):
# Check that we're not being deallocated twice
global double_deallocations
double_deallocations += self.deallocated
self.deallocated = 1
cdef class RecurseSub(Recurse):
"""
>>> recursion_test(RecurseSub)
>>> assert_no_double_deallocations()
"""
cdef int subdeallocated
def __dealloc__(self):
# Check that we're not being deallocated twice
global double_deallocations
double_deallocations += self.subdeallocated
self.subdeallocated = 1
@cython.freelist(4)
@cython.trashcan(True)
cdef class RecurseFreelist:
"""
>>> recursion_test(RecurseFreelist)
>>> recursion_test(RecurseFreelist, 1000)
>>> assert_no_double_deallocations()
"""
cdef public attr
cdef int deallocated
def __cinit__(self, x):
self.attr = x
def __dealloc__(self):
# Check that we're not being deallocated twice
global double_deallocations
double_deallocations += self.deallocated
self.deallocated = 1
# Subclass of list => uses trashcan by default
# As long as https://github.com/python/cpython/pull/11841 is not fixed,
# this does lead to double deallocations, so we skip that check.
cdef class RecurseList(list):
"""
>>> RecurseList(42)
[42]
>>> recursion_test(RecurseList)
"""
def __init__(self, x):
super().__init__((x,))
# Some tests where the trashcan is NOT used. When the trashcan is not used
# in a big recursive deallocation, the __dealloc__s of the base classes are
# only run after the __dealloc__s of the subclasses.
# We use this to detect trashcan usage.
cdef int base_deallocated = 0
cdef int trashcan_used = 0
def assert_no_trashcan_used():
global base_deallocated, trashcan_used
err = trashcan_used
trashcan_used = base_deallocated = 0
assert not err
cdef class Base:
def __dealloc__(self):
global base_deallocated
base_deallocated = 1
# Trashcan disabled by default
cdef class Sub1(Base):
"""
>>> recursion_test(Sub1, 100)
>>> assert_no_trashcan_used()
"""
cdef public attr
def __cinit__(self, x):
self.attr = x
def __dealloc__(self):
global base_deallocated, trashcan_used
trashcan_used += base_deallocated
@cython.trashcan(True)
cdef class Middle(Base):
cdef public foo
# Trashcan disabled explicitly
@cython.trashcan(False)
cdef class Sub2(Middle):
"""
>>> recursion_test(Sub2, 1000)
>>> assert_no_trashcan_used()
"""
cdef public attr
def __cinit__(self, x):
self.attr = x
def __dealloc__(self):
global base_deallocated, trashcan_used
trashcan_used += base_deallocated
|