Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f1b3c94
First variant of stackref tests
efimov-mikhail Oct 7, 2025
871c013
Fixes for Py_STACKREF_DEBUG build
efimov-mikhail Oct 7, 2025
0a82e96
Improvements
efimov-mikhail Oct 7, 2025
a5e39e8
Do not check flags on FT build
efimov-mikhail Oct 7, 2025
f2bd853
Merge branch 'main' into stackref_tests
sergey-miryanov Oct 23, 2025
6feef7a
Review addressed
efimov-mikhail Oct 27, 2025
577c137
Merge branch 'main' into stackref_tests
efimov-mikhail Oct 27, 2025
6390e57
Build fix
efimov-mikhail Oct 27, 2025
e5ec87c
Merge branch 'main' into stackref_tests
efimov-mikhail Oct 27, 2025
b7d7a96
Merge branch 'main' into stackref_tests
efimov-mikhail Oct 28, 2025
f0a09c3
Review addressed
efimov-mikhail Oct 28, 2025
bf07654
Some comments are added
efimov-mikhail Oct 28, 2025
f53afa6
Wording
efimov-mikhail Oct 28, 2025
d4366c5
Merge branch 'main' into stackref_tests
efimov-mikhail Nov 5, 2025
1cb4607
Merge branch 'main' into stackref_tests
efimov-mikhail Nov 13, 2025
1554a94
Merge branch 'main' into stackref_tests
efimov-mikhail Nov 13, 2025
e45d254
Merge branch 'main' into stackref_tests
efimov-mikhail Nov 14, 2025
b1b3b2b
Changes in Py_BuildValue calls
efimov-mikhail Nov 17, 2025
d965529
Changes in Py_BuildValue calls 2
efimov-mikhail Nov 17, 2025
77c88bb
Improvements in test
efimov-mikhail Nov 17, 2025
06fe751
Improvements in test 2
efimov-mikhail Nov 17, 2025
c9ae051
Remove blank line
efimov-mikhail Nov 17, 2025
474f650
Use -1 in flags only for immortal objects on ft build
efimov-mikhail Nov 17, 2025
5274122
Rename obj -> res in aux functions
efimov-mikhail Nov 17, 2025
2471609
Use constants in test
efimov-mikhail Nov 17, 2025
36a2fb4
Remove blank line
efimov-mikhail Nov 17, 2025
14521a1
Asserts on mortality
efimov-mikhail Nov 17, 2025
db34b87
Comment change
efimov-mikhail Nov 17, 2025
63ea804
Comments improvement
efimov-mikhail Nov 18, 2025
72ec253
Merge branch 'main' into stackref_tests
efimov-mikhail Nov 18, 2025
b927d08
Merge branch 'main' into stackref_tests
efimov-mikhail Dec 20, 2025
2998fed
Lint fix
efimov-mikhail Dec 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Include/internal/pycore_stackref.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ static const _PyStackRef PyStackRef_ERROR = { .index = (1 << Py_TAGGED_SHIFT) };

#define INITIAL_STACKREF_INDEX (5 << Py_TAGGED_SHIFT)

static inline PyObject *
_PyStackRef_AsTuple(_PyStackRef ref, PyObject *op)
{
int flags = ref.index & Py_TAG_BITS;
return Py_BuildValue("(Ii)", Py_REFCNT(op), flags);
}

static inline _PyStackRef
PyStackRef_Wrap(void *ptr)
{
Expand Down Expand Up @@ -469,6 +476,13 @@ static const _PyStackRef PyStackRef_NULL = { .bits = Py_TAG_DEFERRED};

#define PyStackRef_IsNullOrInt(stackref) (PyStackRef_IsNull(stackref) || PyStackRef_IsTaggedInt(stackref))

static inline PyObject *
_PyStackRef_AsTuple(_PyStackRef ref, PyObject *op)
{
// Do not check StackRef flags in the free threading build.
return Py_BuildValue("(Ii)", Py_REFCNT(op), -1);
}

static inline PyObject *
PyStackRef_AsPyObjectBorrow(_PyStackRef stackref)
{
Expand Down Expand Up @@ -653,6 +667,13 @@ static const _PyStackRef PyStackRef_NULL = { .bits = PyStackRef_NULL_BITS };
#define PyStackRef_IsFalse(REF) ((REF).bits == (((uintptr_t)&_Py_FalseStruct) | Py_TAG_REFCNT))
#define PyStackRef_IsNone(REF) ((REF).bits == (((uintptr_t)&_Py_NoneStruct) | Py_TAG_REFCNT))

static inline PyObject *
_PyStackRef_AsTuple(_PyStackRef ref, PyObject *op)
{
int flags = ref.bits & Py_TAG_BITS;
return Py_BuildValue("(Ii)", Py_REFCNT(op), flags);
}

#ifdef Py_DEBUG

static inline void PyStackRef_CheckValid(_PyStackRef ref) {
Expand Down
59 changes: 59 additions & 0 deletions Lib/test/test_stackrefs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import unittest
import sys
from test.support import cpython_only
try:
import _testinternalcapi
except ImportError:
_testinternalcapi = None

@cpython_only
class TestDefinition(unittest.TestCase):

def test_equivalence(self):
def run_with_refcount_check(self, func, obj):
refcount = sys.getrefcount(obj)
res = func(obj)
self.assertEqual(sys.getrefcount(obj), refcount)
return res

funcs_with_incref = [
_testinternalcapi.stackref_from_object_new,
_testinternalcapi.stackref_from_object_steal_with_incref,
_testinternalcapi.stackref_make_heap_safe,
_testinternalcapi.stackref_make_heap_safe_with_borrow,
_testinternalcapi.stackref_strong_reference,
]

funcs_with_borrow = [
_testinternalcapi.stackref_from_object_borrow,
_testinternalcapi.stackref_dup_borrowed_with_close,
]

immortal_objs = (None, True, False, 42, '1')

for obj in immortal_objs:
results = set()
for func in funcs_with_incref + funcs_with_borrow:
res = run_with_refcount_check(self, func, obj)
results.add(res)
self.assertEqual(len(results), 1)

mortal_objs = (5000, 3+2j, range(10), object())

for obj in mortal_objs:
results = set()
for func in funcs_with_incref:
res = run_with_refcount_check(self, func, obj)
results.add(res)
self.assertEqual(len(results), 1)

results = set()
for func in funcs_with_borrow:
res = run_with_refcount_check(self, func, obj)
results.add(res)
self.assertEqual(len(results), 1)



if __name__ == "__main__":
unittest.main()
89 changes: 88 additions & 1 deletion Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "pycore_pylifecycle.h" // _PyInterpreterConfig_InitFromDict()
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_runtime_structs.h" // _PY_NSMALLPOSINTS
#include "pycore_stackref.h" // PyStackRef_FunctionCheck()
#include "pycore_unicodeobject.h" // _PyUnicode_TransformDecimalAndSpaceToASCII()

#include "clinic/_testinternalcapi.c.h"
Expand Down Expand Up @@ -2446,7 +2447,6 @@ module_get_gc_hooks(PyObject *self, PyObject *arg)
return result;
}


static void
check_threadstate_set_stack_protection(PyThreadState *tstate,
void *start, size_t size)
Expand Down Expand Up @@ -2497,6 +2497,86 @@ test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args))
Py_RETURN_NONE;
}

static PyObject *
stackref_from_object_new(PyObject *self, PyObject *op)
{
_PyStackRef ref = PyStackRef_FromPyObjectNew(op);
PyObject *obj = _PyStackRef_AsTuple(ref, op);
PyStackRef_CLOSE(ref);
return obj;
}

static PyObject *
stackref_from_object_steal_with_incref(PyObject *self, PyObject *op)
{
// The next two lines are equivalent to PyStackRef_FromPyObjectNew.
Py_INCREF(op);
_PyStackRef ref = PyStackRef_FromPyObjectSteal(op);
PyObject *obj = _PyStackRef_AsTuple(ref, op);
// The next two lines are equivalent to PyStackRef_CLOSE.
PyObject *op2 = PyStackRef_AsPyObjectSteal(ref);
Py_DECREF(op2);
return obj;
}

static PyObject *
stackref_make_heap_safe(PyObject *self, PyObject *op)
{
_PyStackRef ref = PyStackRef_FromPyObjectNew(op);
// For no-GIL release build ref2 is equal to ref.
_PyStackRef ref2 = PyStackRef_MakeHeapSafe(ref);
PyObject *obj = _PyStackRef_AsTuple(ref2, op);
PyStackRef_CLOSE(ref2);
return obj;
}

static PyObject *
stackref_make_heap_safe_with_borrow(PyObject *self, PyObject *op)
{
_PyStackRef ref = PyStackRef_FromPyObjectNew(op);
_PyStackRef ref2 = PyStackRef_Borrow(ref);
_PyStackRef ref3 = PyStackRef_MakeHeapSafe(ref2);
// We can close ref, since ref3 is heap safe.
PyStackRef_CLOSE(ref);
PyObject *obj = _PyStackRef_AsTuple(ref3, op);
PyStackRef_CLOSE(ref3);
return obj;
}

static PyObject *
stackref_strong_reference(PyObject *self, PyObject *op)
{
// The next two lines are equivalent to op2 = Py_NewRef(op).
_PyStackRef ref = PyStackRef_FromPyObjectBorrow(op);
PyObject *op2 = PyStackRef_AsPyObjectSteal(ref);
_PyStackRef ref2 = PyStackRef_FromPyObjectSteal(op2);
PyObject *obj = _PyStackRef_AsTuple(ref2, op);
PyStackRef_CLOSE(ref2);
return obj;
}

static PyObject *
stackref_from_object_borrow(PyObject *self, PyObject *op)
{
_PyStackRef ref = PyStackRef_FromPyObjectBorrow(op);
PyObject *obj = _PyStackRef_AsTuple(ref, op);
PyStackRef_CLOSE(ref);
return obj;
}

static PyObject *
stackref_dup_borrowed_with_close(PyObject *self, PyObject *op)
{
_PyStackRef ref = PyStackRef_FromPyObjectBorrow(op);
// For no-GIL release build ref2 is equal to ref.
_PyStackRef ref2 = PyStackRef_DUP(ref);
// For release build closing borrowed ref is no-op.
PyStackRef_XCLOSE(ref);
PyObject *obj = _PyStackRef_AsTuple(ref2, op);
PyStackRef_XCLOSE(ref2);
return obj;
}


static PyMethodDef module_functions[] = {
{"get_configs", get_configs, METH_NOARGS},
Expand Down Expand Up @@ -2610,6 +2690,13 @@ static PyMethodDef module_functions[] = {
{"module_get_gc_hooks", module_get_gc_hooks, METH_O},
{"test_threadstate_set_stack_protection",
test_threadstate_set_stack_protection, METH_NOARGS},
{"stackref_from_object_new", stackref_from_object_new, METH_O},
{"stackref_from_object_steal_with_incref", stackref_from_object_steal_with_incref, METH_O},
{"stackref_make_heap_safe", stackref_make_heap_safe, METH_O},
{"stackref_make_heap_safe_with_borrow", stackref_make_heap_safe_with_borrow, METH_O},
{"stackref_strong_reference", stackref_strong_reference, METH_O},
{"stackref_from_object_borrow", stackref_from_object_borrow, METH_O},
{"stackref_dup_borrowed_with_close", stackref_dup_borrowed_with_close, METH_O},
{NULL, NULL} /* sentinel */
};

Expand Down
Loading