Skip to content

Adding tp_reachable#65

Open
mjp41 wants to merge 6 commits intoimmutable-mainfrom
tp_reaches
Open

Adding tp_reachable#65
mjp41 wants to merge 6 commits intoimmutable-mainfrom
tp_reaches

Conversation

@mjp41
Copy link
Owner

@mjp41 mjp41 commented Jan 16, 2026


📚 Documentation preview 📚: https://cpython-previews--65.org.readthedocs.build/

Copy link
Collaborator

@xFrednet xFrednet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition itself looks good, but it doesn't set the tp_reachable for most built-in types. Once that is done it should be good to go.

@mjp41
Copy link
Owner Author

mjp41 commented Jan 19, 2026

The addition itself looks good, but it doesn't set the tp_reachable for most built-in types. Once that is done it should be good to go.

Sounds good. I mostly wanted to get someone else's eyes on it, and then we can decide how broad a blast radius we want.

Comment on lines 132 to 143
static PyObject *
immutable_clear_tp_reachable(PyObject *module, PyObject *obj)
{
if (!PyType_Check(obj)) {
PyErr_SetString(PyExc_TypeError, "Expected a type");
return NULL;
}
PyTypeObject *tp = (PyTypeObject *)obj;
tp->tp_reachable = NULL;
Py_RETURN_NONE;
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I am happy exposing this for a test. We should find a better way to test this. I think we really need to build a C-Module that doesn't have tp_reachable, and never will.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are c-modules in CPython which are only used for testing. They expose C Types for testing. Would that be sufficient?

Comment on lines +2502 to +2507
if (co->_co_cached != NULL) {
Py_VISIT(co->_co_cached->_co_code);
Py_VISIT(co->_co_cached->_co_varnames);
Py_VISIT(co->_co_cached->_co_cellvars);
Py_VISIT(co->_co_cached->_co_freevars);
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me worry about write barriers on the cache. There is a lot of stuff around modifying this for NoGIL

Comment on lines 8386 to 8390
/*
* TODO: We should discuss this. I am a little confused about the
* correct way to do this. This gives a good level of compatibility,
* but is perhaps too much.
*
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we discuss this?

@mjp41 mjp41 requested a review from xFrednet February 27, 2026 09:41
@mjp41 mjp41 marked this pull request as ready for review February 27, 2026 09:41
Comment on lines +4751 to +4752
static int
dict_reachable(PyObject *op, visitproc visit, void *arg)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: When I implemented this, I used a dict_visit function with a flag to indicate it the keys should be used or not:

cpython/Objects/dictobject.c

Lines 4975 to 5025 in e6fe674

static int
dict_visit(PyObject *op, visitproc visit, void *arg, bool visit_unicode_keys)
{
PyDictObject *mp = (PyDictObject *)op;
PyDictKeysObject *keys = mp->ma_keys;
Py_ssize_t i, n = keys->dk_nentries;
if (DK_IS_UNICODE(keys)) {
if (_PyDict_HasSplitTable(mp)) {
if (!mp->ma_values->embedded) {
for (i = 0; i < n; i++) {
if (visit_unicode_keys) {
Py_VISIT(DK_UNICODE_ENTRIES(mp->ma_keys)[i].me_key);
}
Py_VISIT(mp->ma_values->values[i]);
}
}
}
else {
PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys);
for (i = 0; i < n; i++) {
if (visit_unicode_keys) {
Py_VISIT(entries[i].me_key);
}
Py_VISIT(entries[i].me_value);
}
}
}
else {
PyDictKeyEntry *entries = DK_ENTRIES(keys);
for (i = 0; i < n; i++) {
if (entries[i].me_value != NULL) {
Py_VISIT(entries[i].me_value);
Py_VISIT(entries[i].me_key);
}
}
}
return 0;
}
static int
dict_traverse(PyObject *op, visitproc visit, void *arg)
{
return dict_visit(op, visit, arg, false);
}
int
_PyDict_Reachable(PyObject *op, visitproc visit, void *arg)
{
return dict_visit(op, visit, arg, true);
}

I think that's slightly cleaner than having two mostly identical visit functions, but this is a style thing and this is good to go as is.

Comment on lines +659 to +674
.tp_reachable = BaseException_reachable,
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
BaseException_methods, /* tp_methods */
BaseException_members, /* tp_members */
BaseException_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
offsetof(PyBaseExceptionObject, dict), /* tp_dictoffset */
BaseException_init, /* tp_init */
0, /* tp_alloc */
BaseException_new, /* tp_new */
.tp_vectorcall = BaseException_vectorcall,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems weird, why does the change add all of these? Shouldn't it just be

.tp_reachable = BaseException_reachable,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants