Skip to content

Conversation

@matajoh
Copy link
Collaborator

@matajoh matajoh commented Mar 5, 2025

This draft PR is to give a chance to view the diff between 3.12 and just adding the deep immutability from veronapy. The key difference is the use of the lowest bit of ob_refcnt to indicate immutability (so as to not impact object header size).


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

@matajoh matajoh force-pushed the immutable-pep branch 2 times, most recently from dd6a77c to 469ce9e Compare March 5, 2025 15:54
@matajoh matajoh mentioned this pull request Mar 5, 2025
@mjp41 mjp41 changed the base branch from pyrona to v3.12.0 March 5, 2025 16:53
@matajoh matajoh force-pushed the immutable-pep branch 3 times, most recently from 9d8efcd to e676ca4 Compare March 12, 2025 16:46
@matajoh matajoh force-pushed the immutable-pep branch 3 times, most recently from 8ce6f94 to 295e983 Compare March 28, 2025 12:01
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
@matajoh matajoh marked this pull request as ready for review March 28, 2025 13:47
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.

My comments can be quite nit-picky, so please don't hesitate to push back on them :)

I'm not done reviewing everything yet, but from what I've seen thus far: Excellent job! It's amazing that you managed to get all of this together, well done :D
This review is still incomplete. From all I've read, excellent job!

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.

Very nicely done! This is mainly a collection of questions, which probably come from me missing some context.

@matajoh matajoh force-pushed the immutable-pep branch 2 times, most recently from e269f3f to 1cdd289 Compare April 3, 2025 10:22
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
matajoh added 3 commits April 3, 2025 16:33
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Comment on lines 167 to 178
}else if(PyDict_Contains(module_dict, name)){
PyObject* value = PyDict_GetItem(module_dict, name); // value.rc = x

_PyDict_SetKeyImmutable((PyDictObject*)module_dict, name);

if(!_Py_IsImmutable(value)){
if(PyList_Append(frontier, value)){
goto nomemory;
}
}
}
}
Copy link
Owner

Choose a reason for hiding this comment

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

I don't understand what this is doing? Could you explain an example that this is catching that is a problem?

Comment on lines 184 to 194
}else if(PyDict_Contains(module_dict, name)){
PyObject* value = PyDict_GetItem(module_dict, name);

_PyDict_SetKeyImmutable((PyDictObject*)module_dict, name);

if(!_Py_IsImmutable(value)){
if(push(frontier, value)){
goto nomemory;
}
}
}
Copy link
Owner

Choose a reason for hiding this comment

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

I couldn't work out how to exercise this code. It doesn't appear to be hit by test_freeze. Should we be able to get coverage for this? I had an experiment with the REPL and a few modules, but this code path didn't get exercised.

matajoh added 2 commits April 9, 2025 14:08
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
matajoh and others added 4 commits May 1, 2025 10:59
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
matajoh added 4 commits May 2, 2025 11:35
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
struct _Py_async_gen_state async_gen;
struct _Py_context_state context;
struct _Py_exc_state exc_state;
struct _Py_immutability_state immutability;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this mean that different interpreters have different views on what is freezable?

return;
}
ob->ob_refcnt = refcnt;
ob->ob_refcnt = (ob->ob_refcnt & _Py_IMMUTABLE_MASK) | (refcnt & _Py_REFCNT_MASK);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't the immutability flag always be 0 here due to the if statement above?

@xFrednet
Copy link
Collaborator

xFrednet commented May 8, 2025

@matajoh Do we want to change the title of the PR to be ready for outside viewers? Maybe something like "Adding Deep Immutability to 3.12"

Comment on lines +1762 to +1767
if (!Py_CHECKWRITE(op)) { \
PyErr_SetObject( \
PyExc_TypeError, \
(PyObject *)((op))); \
return -1; \
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why does this create an Error object and doesn't use PyErr_WriteToImmutable like the additions in the other files?

Also why is it called _VALIDATE_WRITABLE here and _CHECK_IS_WRITABLE above? The difference seems to be the return value, but they seem to be used in the same way.

if (type == NULL) { \
goto error; \
} \
#define CREATE_TYPE(module, type, spec, base) \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this new base parameter still needed? It seems to be always NULL in the uses below

Comment on lines +2506 to +2508
if (_PyImmutability_RegisterFreezable((PyTypeObject *)state->PyStructType) < 0){
return -1;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

The file Modules/_elementtree.c uses _PyImport_GetModuleAttrString("immutable", "register_freezable"); to get the register_freezable function or handle failure if the "immutable" module is not available. This one uses the builtin function _PyImmutability_RegisterFreezable from pycore. How is it decided which function is used?

Comment on lines +592 to +596
if(self->codec->encinit != NULL){
if(!Py_CHECKWRITE(self)){
return PyErr_WriteToImmutable(self);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This file has new Py_CHECKWRITE but doesn't register the type as freezable. Is this declared somewhere else?

Comment on lines +5 to +8
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
# include "pycore_gc.h" // PyGC_Head
# include "pycore_runtime.h" // _Py_ID()
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

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

These two includes don't seem to be used in the header file, can we move them into the .c file instead?

#include "clinic/immutablemodule.c.h"

typedef struct {
PyObject *not_freezable_error_obj;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this object still used? Looking at the code, it seems to only be initialized and cleared, but never used outsite of this

Comment on lines +37 to +46
static PyObject *
immutable_error(void)
{
PyThreadState *tstate = _PyThreadState_GET();
if (!_PyErr_Occurred(tstate)) {
_PyErr_SetString(tstate, PyExc_TypeError,
"cannot modify immutable instance");
}
return NULL;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why does this used a custom immutable_error function and not PyErr_WriteToImmutable(). The difference seems to be that this function doesn't take the object that caused the error. But looking at the Py_CHECKWRITE(o) we always seem to have an object

Comment on lines +68 to +71
if(PyDict_SetItemString(PyModule_GetDict(frozen_importlib), "_freezable_types", state->freezable_types)){
Py_DECREF(frozen_importlib);
return -1;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

My understanding is, that this exposes the freezable_types set via the frozen_importlib module. Is there a reason for this?

size = PySequence_Fast_GET_SIZE(f_code->co_consts);
for(Py_ssize_t i = 0; i < size; i++){
PyObject* value = PySequence_Fast_GET_ITEM(f_code->co_consts, i);
if(check_globals && PyUnicode_Check(value)){
Copy link
Collaborator

Choose a reason for hiding this comment

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

Couldn't we move the check_globals check outside the for loop?

Comment on lines +530 to +532
if(_Py_IsImmutable(item)){
continue;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can't we move this check above check_freezable? It seems like it's way cheaper.

Signed-off-by: Matthew A Johnson <matjoh@microsoft.com>
@matajoh matajoh changed the title Code diff with just the immutability changes Adding Deep Immutability to 3.12 May 13, 2025
@alwaysmpe
Copy link

alwaysmpe commented Jul 2, 2025

I've been playing with the current implementation. Seems that it’s sometimes trying to freeze type variables that are arguments to generic functions. And that can’t be done. A simple decorator example:

from __future__ import annotations
from immutable import freeze
from typing import Callable
def foo[**P, R](fn: Callable[P, R]) -> Callable[P, R]:
    def fn_wrap(*args: P.args, **kwargs: P.kwargs) -> R:
        return fn(*args, **kwargs)
    return fn_wrap
freeze(foo) # TypeError: Cannot freeze instance of type typing.ParamSpec due to custom functionality implemented in C

I’m surprised that from __future__ import annotations doesn’t make a difference.

@mjp41
Copy link
Owner

mjp41 commented Jul 2, 2025

I've been playing with the current implementation. Seems that it’s sometimes trying to freeze type variables that are arguments to generic functions. And that can’t be done. A simple decorator example:

from __future__ import annotations
from immutable import freeze
from typing import Callable
def foo[**P, R](fn: Callable[P, R]) -> Callable[P, R]:
    def fn_wrap(*args: P.args, **kwargs: P.kwargs) -> R:
        return fn(*args, **kwargs)
    return fn_wrap
freeze(foo) # TypeError: Cannot freeze instance of type typing.ParamSpec due to custom functionality implemented in C

I’m surprised that from __future__ import annotations doesn’t make a difference.

Thanks for raising this. I'll investigate what is happening, and get back to you.

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.

4 participants