Skip to content

Commit 88f13d2

Browse files
authored
Immutable: Add invariant for debugging (#59)
* Ownership: Add debugging invariant with `--with-ownership-invariant` * Review comments <3 * sudo CI=green
1 parent 22df1e0 commit 88f13d2

18 files changed

+444
-46
lines changed

Include/internal/pycore_interp_structs.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ extern "C" {
1111
#include "pycore_immutability.h" // struct _immutability_runtime_state
1212
#include "pycore_llist.h" // struct llist_node
1313
#include "pycore_opcode_utils.h" // NUM_COMMON_CONSTANTS
14+
#include "pycore_ownership.h" // struct _Py_ownership_state
1415
#include "pycore_pymath.h" // _PY_SHORT_FLOAT_REPR
1516
#include "pycore_structs.h" // PyHamtObject
1617
#include "pycore_tstate.h" // _PyThreadStateImpl
@@ -937,6 +938,7 @@ struct _is {
937938
struct _Py_exc_state exc_state;
938939
struct _Py_immutability_state immutability;
939940
struct _Py_mem_interp_free_queue mem_free_queue;
941+
_Py_ownership_state ownership;
940942

941943
struct ast_state ast;
942944
struct types_state types;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#ifndef Py_INTERNAL_OWNERSHIP_H
2+
#define Py_INTERNAL_OWNERSHIP_H
3+
#ifdef __cplusplus
4+
extern "C" {
5+
#endif
6+
7+
#ifndef Py_BUILD_CORE
8+
# error "Py_BUILD_CORE must be defined to include this header"
9+
#endif
10+
11+
#include "exports.h"
12+
13+
typedef struct _Py_ownership_state {
14+
/* Temporary value until the state always has a field to indicate this. */
15+
int is_initialized;
16+
#ifdef Py_OWNERSHIP_INVARIANT
17+
/* Tracks the state of the ownership invariant. Some ownership-related
18+
* operations may temporarily violate the invariant. To handle this safely,
19+
* the invariant must be suspended during such operations and only resumed
20+
* once all of them complete. This is necessary to support re-entrancy.
21+
*
22+
* For example, during freezing, the object graph is traversed and objects
23+
* are marked as immutable — even while they may still reference mutable
24+
* objects. If the invariant were enforced mid-way, it would raise a
25+
* (premature) error, despite the state being corrected as the operation
26+
* completes. To avoid this, the invariant must be paused during the freeze.
27+
*
28+
* States:
29+
* -1 => The invariant is disabled.
30+
* 0 => The invariant is active and enforced.
31+
* N => The invariant is temporarily paused. The value indicates the
32+
* number of suspensions yet to be resumed (this supports nesting).
33+
*/
34+
int invariant_state;
35+
#endif
36+
} _Py_ownership_state;
37+
38+
/* This function returns true for C wrappers around functions, types and
39+
* all kinds of wrappers around C with immutable state. For ownership these
40+
* can be seen as immutable, meaning they can be referenced from immutable
41+
* objects and from inside regions.
42+
*/
43+
PyAPI_FUNC(int) _PyOwnership_is_c_wrapper(PyObject *obj);
44+
45+
/* This function calls the `visit` function for the fields of the `obj`
46+
* which should be effected by ownership. The `data` pointer will be
47+
* passed along as the second argument to `visit`.
48+
*/
49+
PyAPI_FUNC(int) _PyOwnership_traverse_obj(PyObject *obj, visitproc visit, void *data);
50+
51+
#ifdef Py_OWNERSHIP_INVARIANT
52+
53+
#include "object.h" // PyObject, visitproc
54+
#include "pytypedefs.h" // PyThreadState
55+
56+
#define Py_OWNERSHIP_INVARIANT_DISABLED -1
57+
#define Py_OWNERSHIP_INVARIANT_ENABLED 0
58+
59+
/* This function validates that the current heap follows the ownership
60+
* rules. This is a slow operation and should only be done for debugging.
61+
*
62+
* 0 indicates a valid heap, -1 will be returned if an error was thrown.
63+
*/
64+
PyAPI_FUNC(int) _PyOwnership_check_invariant(PyThreadState *tstate);
65+
66+
PyAPI_FUNC(int) _PyOwnership_invariant_enable(void);
67+
PyAPI_FUNC(int) _PyOwnership_invariant_pause(void);
68+
PyAPI_FUNC(int) _PyOwnership_invariant_resume(void);
69+
70+
#else
71+
# define _PyOwnership_invariant_enable() 0 /* success */
72+
# define _PyOwnership_invariant_pause() 0 /* success */
73+
# define _PyOwnership_invariant_resume() 0 /* success */
74+
#endif
75+
76+
#ifdef __cplusplus
77+
}
78+
#endif
79+
#endif /* !Py_INTERNAL_OWNERSHIP_H */

Lib/test/test_freeze/test_core.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -465,15 +465,6 @@ def test_weakref(self):
465465
# self.assertTrue(c.val() is obj)
466466
self.assertIsNone(c.val())
467467

468-
class TestStackCapture(unittest.TestCase):
469-
def test_stack_capture(self):
470-
import sys
471-
x = {}
472-
x["frame"] = sys._getframe()
473-
freeze(x)
474-
self.assertTrue(isfrozen(x))
475-
self.assertTrue(isfrozen(x["frame"]))
476-
477468
global_test_dict = 0
478469
class TestGlobalDictMutation(unittest.TestCase):
479470
def g():

Lib/test/test_freeze/test_gc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from gc import collect
22
import unittest
3-
from immutable import freeze, NotFreezable, isfrozen
3+
from immutable import freeze
44

55
class GCInteropTest(unittest.TestCase):
66
def test_collect(self):
@@ -11,4 +11,4 @@ def test_collect(self):
1111
# Freeze it
1212
freeze(a)
1313
# f
14-
collect()
14+
collect()

Makefile.pre.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ PYTHON_OBJS= \
474474
Python/optimizer.o \
475475
Python/optimizer_analysis.o \
476476
Python/optimizer_symbols.o \
477+
Python/ownership.o \
477478
Python/parking_lot.o \
478479
Python/pathconfig.o \
479480
Python/preconfig.o \
@@ -1356,6 +1357,7 @@ PYTHON_HEADERS= \
13561357
$(srcdir)/Include/internal/pycore_opcode_metadata.h \
13571358
$(srcdir)/Include/internal/pycore_opcode_utils.h \
13581359
$(srcdir)/Include/internal/pycore_optimizer.h \
1360+
$(srcdir)/Include/internal/pycore_ownership.h \
13591361
$(srcdir)/Include/internal/pycore_parking_lot.h \
13601362
$(srcdir)/Include/internal/pycore_parser.h \
13611363
$(srcdir)/Include/internal/pycore_pathconfig.h \

PCbuild/_freeze_module.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
<ClCompile Include="..\Python\optimizer.c" />
244244
<ClCompile Include="..\Python\optimizer_analysis.c" />
245245
<ClCompile Include="..\Python\optimizer_symbols.c" />
246+
<ClCompile Include="..\Python\ownership.c" />
246247
<ClCompile Include="..\Python\parking_lot.c" />
247248
<ClCompile Include="..\Python\pathconfig.c" />
248249
<ClCompile Include="..\Python\perf_trampoline.c" />

PCbuild/_freeze_module.vcxproj.filters

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@
328328
<ClCompile Include="..\Python\optimizer_symbols.c">
329329
<Filter>Python</Filter>
330330
</ClCompile>
331+
<ClCompile Include="..\Python\ownership.c">
332+
<Filter>Source Files</Filter>
333+
</ClCompile>
331334
<ClCompile Include="..\Parser\parser.c">
332335
<Filter>Source Files</Filter>
333336
</ClCompile>

PCbuild/pythoncore.vcxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@
285285
<ClInclude Include="..\Include\internal\pycore_obmalloc.h" />
286286
<ClInclude Include="..\Include\internal\pycore_obmalloc_init.h" />
287287
<ClInclude Include="..\Include\internal\pycore_optimizer.h" />
288+
<ClInclude Include="..\Include\internal\pycore_ownership.h" />
288289
<ClInclude Include="..\Include\internal\pycore_parking_lot.h" />
289290
<ClInclude Include="..\Include\internal\pycore_pathconfig.h" />
290291
<ClInclude Include="..\Include\internal\pycore_pyarena.h" />
@@ -640,6 +641,7 @@
640641
<ClCompile Include="..\Python\optimizer.c" />
641642
<ClCompile Include="..\Python\optimizer_analysis.c" />
642643
<ClCompile Include="..\Python\optimizer_symbols.c" />
644+
<ClCompile Include="..\Python\ownership.c" />
643645
<ClCompile Include="..\Python\parking_lot.c" />
644646
<ClCompile Include="..\Python\pathconfig.c" />
645647
<ClCompile Include="..\Python\perf_trampoline.c" />

PCbuild/pythoncore.vcxproj.filters

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,8 @@
694694
<Filter>Include\internal</Filter>
695695
</ClInclude>
696696
<ClInclude Include="..\Include\internal\pycore_index_pool.h">
697+
<Filter>Include\internal</Filter>
698+
</ClInclude>
697699
<ClInclude Include="..\Include\internal\pycore_immutability.h">
698700
<Filter>Include\internal</Filter>
699701
</ClInclude>
@@ -1427,6 +1429,8 @@
14271429
<Filter>Python</Filter>
14281430
</ClCompile>
14291431
<ClCompile Include="..\Python\index_pool.c">
1432+
<Filter>Python</Filter>
1433+
</ClCompile>
14301434
<ClCompile Include="..\Python\immutability.c">
14311435
<Filter>Python</Filter>
14321436
</ClCompile>
@@ -1478,6 +1482,9 @@
14781482
<ClCompile Include="..\Python\optimizer_symbols.c">
14791483
<Filter>Python</Filter>
14801484
</ClCompile>
1485+
<ClCompile Include="..\Python\ownership.c">
1486+
<Filter>Python</Filter>
1487+
</ClCompile>
14811488
<ClCompile Include="..\Python\parking_lot.c">
14821489
<Filter>Python</Filter>
14831490
</ClCompile>

Python/ceval.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "pycore_opcode_metadata.h" // EXTRA_CASES
3030
#include "pycore_opcode_utils.h" // MAKE_FUNCTION_*
3131
#include "pycore_optimizer.h" // _PyUOpExecutor_Type
32+
#include "pycore_ownership.h" // _PyOwnership_check_invariant
3233
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_*
3334
#include "pycore_pyerrors.h" // _PyErr_GetRaisedException()
3435
#include "pycore_pystate.h" // _PyInterpreterState_GET()

0 commit comments

Comments
 (0)