Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
5a3b525
Proposed code changes for [PEP XXX](link).
matajoh Mar 28, 2025
5e8f428
Add Debug trace for where something was frozen.
mjp41 Jun 4, 2025
78374f9
Disable Test Decimal for now
mjp41 Jun 5, 2025
1bb7711
Add backtracking
mjp41 Jun 10, 2025
870f10e
Refactor
mjp41 Jun 12, 2025
c78046e
More refactorings
mjp41 Jun 13, 2025
7034fb9
More refactorings
mjp41 Jun 13, 2025
6ef19fa
fixup
mjp41 Jun 13, 2025
cab7dd8
CI
mjp41 Jun 13, 2025
9decd2d
CI
mjp41 Jun 13, 2025
c9ecaff
Fix bug from bad interaction with incremental GC.
mjp41 Jul 14, 2025
aa72f83
Changes made in sprint for PLDI deadline. (#62)
mjp41 Dec 17, 2025
e4df334
Create a Python surface to the ImmutableModule
mjp41 Jan 15, 2026
7e9eb4e
Expose mutable module states via `sys.mut_modules`
xFrednet Feb 27, 2026
72744eb
Immutability: Make `shadow_function_globals` less smart and failable
xFrednet Mar 2, 2026
6e5eea8
Immutability: Make `shadow_function_globals` less smart and failable
xFrednet Mar 2, 2026
be3a542
Make CI run for *-main (#70)
mjp41 Mar 3, 2026
47e7c4b
Handle immutable symbols. (#72)
mjp41 Mar 3, 2026
3f5bf95
Adding tp_reachable (#65)
mjp41 Mar 4, 2026
0165c58
Immutable modules: Fix proxy mode breaking tests
xFrednet Mar 3, 2026
6c05a4e
Only run `test_ctypes` if ctypes are available
xFrednet Mar 3, 2026
30d240f
Immutability: Add module proxy mode tests
xFrednet Mar 3, 2026
e713de1
Merge pull request #71 from mjp41/immutable-proxy-modules
xFrednet Mar 4, 2026
f1f954b
Missing clean up code (#73)
mjp41 Mar 4, 2026
d0aeee0
Add "can be viewed as immutable" query to the immutability system (#74)
mjp41 Mar 5, 2026
e1c6249
Add set_freezable() for per-object freeze policy (#77)
mjp41 Mar 6, 2026
262d06c
Fix freeze rollback to unfreeze completed SCCs on error
mjp41 Mar 6, 2026
dac0a51
Make freeze take multiple arguments
mjp41 Mar 6, 2026
085047e
CI complaints
mjp41 Mar 6, 2026
01b6597
Make freeze return the first argument it is supplied.
mjp41 Mar 6, 2026
32a1390
Add tests freeze return first argument
mjp41 Mar 6, 2026
00b1bb5
Merge pull request #78 from fxpl/immutable-rollback
xFrednet Mar 11, 2026
b6378e0
Immutability: Use ob_refcnt instead of ob_refcnt_full
kulisak12 Mar 5, 2026
24a9eb3
Immutability: Make weakref operations thread-safe
kulisak12 Mar 5, 2026
d647ae7
Immutability: Reference counting for weakrefs
kulisak12 Mar 11, 2026
d1ed4df
.gitignore should only ignore the root /build directory
xFrednet Mar 12, 2026
b44b98b
Remove register freezable (#80)
mjp41 Mar 12, 2026
7f3f531
Immutability: Call weakref callbacks
kulisak12 Mar 11, 2026
af07e74
Immutability: More weakref tests
kulisak12 Mar 12, 2026
67342ff
Immutability: Run weakref callbacks directly if possible
kulisak12 Mar 12, 2026
3b50ca9
Immutability: Clear weakrefs in two steps
kulisak12 Mar 13, 2026
9025a92
Immutability: Catch tp_traverse missing types
xFrednet Mar 13, 2026
5c182a6
Merge pull request #75 from kulisak12/immutable-weakrefs
xFrednet Mar 13, 2026
55a2699
Define `Py_tp_reachable` for heap types in CPython
xFrednet Mar 13, 2026
1a6d2cf
Generalize `tp_reachable`
xFrednet Mar 13, 2026
607e295
Add last reachables and improving CI (Still broken though)
xFrednet Mar 14, 2026
2f98900
Merge pull request #84 from fxpl/immutable-more-reachable
xFrednet Mar 14, 2026
86f894f
Merge pull request #83 from fxpl/fix-heap-type-cycles
xFrednet Mar 15, 2026
2ab8994
Correct Segfault introduced by #83
xFrednet Mar 15, 2026
b6c7df3
Merge pull request #86 from fxpl/immutable-segfault-fix-83
xFrednet Mar 15, 2026
cd9415a
Immutability: Clear collecting flag during freezing
xFrednet Mar 5, 2026
d67f752
Immutability: Basic Pre-freeze infrastructure
xFrednet Mar 5, 2026
f7937c0
Ensure that the pre-freeze hook only runs once
xFrednet Mar 6, 2026
f3da03a
pre-freeze: Error on nested calls for MVP
xFrednet Mar 14, 2026
0015ec3
Extract undo_freeze
xFrednet Mar 14, 2026
cfa4210
Treat freezing as transactions to support nested calls
xFrednet Mar 14, 2026
4bb1d80
Fix `IMMUTABLE_TRACING` UB and ASAN errors
xFrednet Mar 14, 2026
28bb471
Pre-freeze: Add tests and fix bug
xFrednet Mar 14, 2026
a9c05fa
Merge pull request #76 from fxpl/immutable-freeze-hook
xFrednet Mar 15, 2026
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
40 changes: 23 additions & 17 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ on:
push:
branches:
- 'main'
- '*-main'
- '3.*'
pull_request:
branches:
- 'main'
- '*-main'
- '3.*'

permissions:
Expand Down Expand Up @@ -168,10 +170,11 @@ jobs:
- arm64
free-threading:
- false
- true
exclude:
# Skip Win32 on free-threaded builds
- { arch: Win32, free-threading: true }
# TODO(Immutable): Enable free-threading build when it is made to work.
# - true
# exclude:
# # Skip Win32 on free-threaded builds
# - { arch: Win32, free-threading: true }
uses: ./.github/workflows/reusable-windows.yml
with:
arch: ${{ matrix.arch }}
Expand Down Expand Up @@ -209,10 +212,11 @@ jobs:
- macos-15-intel
free-threading:
- false
- true
exclude:
- os: macos-15-intel
free-threading: true
# TODO(Immutable): Enable free-threading build when it is made to work.
# - true
# exclude:
# - os: macos-15-intel
# free-threading: true
uses: ./.github/workflows/reusable-macos.yml
with:
config_hash: ${{ needs.build-context.outputs.config-hash }}
Expand All @@ -234,14 +238,15 @@ jobs:
- true
free-threading:
- false
- true
# TODO(Immutable): Enable free-threading build when it is made to work.
# - true
os:
- ubuntu-24.04
- ubuntu-24.04-arm
exclude:
# Do not test BOLT with free-threading, to conserve resources
- bolt: true
free-threading: true
# # Do not test BOLT with free-threading, to conserve resources
# - bolt: true
# free-threading: true
# BOLT currently crashes during instrumentation on aarch64
- os: ubuntu-24.04-arm
bolt: true
Expand Down Expand Up @@ -614,13 +619,14 @@ jobs:
- Thread
free-threading:
- false
- true
# TODO(Immutable): Enable free-threading build when it is made to work.
# - true
sanitizer:
- TSan
include:
- check-name: Undefined behavior
sanitizer: UBSan
free-threading: false
# include:
# - check-name: Undefined behavior
# sanitizer: UBSan
# free-threading: false
uses: ./.github/workflows/reusable-san.yml
with:
sanitizer: ${{ matrix.sanitizer }}
Expand Down
54 changes: 27 additions & 27 deletions .github/workflows/jit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,33 +129,33 @@ jobs:
make all --jobs 4
./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3

jit-with-disabled-gil:
name: Free-Threaded (Debug)
needs: interpreter
runs-on: ubuntu-24.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
llvm:
- 19
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build with JIT enabled and GIL disabled
run: |
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
./configure --enable-experimental-jit --with-pydebug --disable-gil
make all --jobs 4
- name: Run tests
run: |
./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
continue-on-error: true
# jit-with-disabled-gil:
# name: Free-Threaded (Debug)
# needs: interpreter
# runs-on: ubuntu-24.04
# timeout-minutes: 90
# strategy:
# fail-fast: false
# matrix:
# llvm:
# - 19
# steps:
# - uses: actions/checkout@v4
# with:
# persist-credentials: false
# - uses: actions/setup-python@v5
# with:
# python-version: '3.11'
# - name: Build with JIT enabled and GIL disabled
# run: |
# sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
# export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
# ./configure --enable-experimental-jit --with-pydebug --disable-gil
# make all --jobs 4
# - name: Run tests
# run: |
# ./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
# continue-on-error: true

no-opt-jit:
name: JIT without optimizations (Debug)
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/tail-call.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,11 @@ jobs:
make all --jobs 4
./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3

- name: Native Linux with free-threading (release)
if: matrix.target == 'free-threading'
run: |
sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
CC=clang-20 ./configure --with-tail-call-interp --disable-gil
make all --jobs 4
./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
# - name: Native Linux with free-threading (release)
# if: matrix.target == 'free-threading'
# run: |
# sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh ${{ matrix.llvm }}
# export PATH="$(llvm-config-${{ matrix.llvm }} --bindir):$PATH"
# CC=clang-20 ./configure --with-tail-call-interp --disable-gil
# make all --jobs 4
# ./python -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
3 changes: 3 additions & 0 deletions Doc/includes/typestruct.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,7 @@ typedef struct _typeobject {
* Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
*/
uint16_t tp_versions_used;

/* call function for all referenced objects (includes non-cyclic refs) */
traverseproc tp_reachable;
} PyTypeObject;
1 change: 1 addition & 0 deletions Include/Python.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ __pragma(warning(disable: 4201))
#include "fileutils.h"
#include "cpython/pyfpe.h"
#include "cpython/tracemalloc.h"
#include "immutability.h"

#ifdef _MSC_VER
__pragma(warning(pop)) // warning(disable: 4201)
Expand Down
13 changes: 13 additions & 0 deletions Include/cpython/immutability.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef Py_CPYTHON_IMMUTABLE_H
# error "this header file must not be included directly"
#endif

PyAPI_DATA(PyTypeObject) _PyNotFreezable_Type;

PyAPI_FUNC(int) _PyImmutability_Freeze(PyObject*);
PyAPI_FUNC(int) _PyImmutability_FreezeMany(PyObject *const *, Py_ssize_t);
PyAPI_FUNC(int) _PyImmutability_RegisterFreezable(PyTypeObject*);
PyAPI_FUNC(int) _PyImmutability_RegisterShallowImmutable(PyTypeObject*);
PyAPI_FUNC(int) _PyImmutability_CanViewAsImmutable(PyObject*);
PyAPI_FUNC(int) _PyImmutability_SetFreezable(PyObject *, int);
PyAPI_FUNC(int) _PyImmutability_GetFreezable(PyObject *);
1 change: 1 addition & 0 deletions Include/cpython/listobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ PyList_SET_ITEM(PyObject *op, Py_ssize_t index, PyObject *value) {
PyListObject *list = _PyList_CAST(op);
assert(0 <= index);
assert(index < list->allocated);
// TODO(Immutable): Add assert to check if the list is immutable
list->ob_item[index] = value;
}
#define PyList_SET_ITEM(op, index, value) \
Expand Down
7 changes: 7 additions & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ struct _typeobject {
* Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
*/
uint16_t tp_versions_used;

/* call function for all referenced objects (includes non-cyclic refs) */
traverseproc tp_reachable;

/* A callback called before a type is frozen. */
prefreezeproc tp_prefreeze;
};

#define _Py_ATTR_CACHE_UNUSED (30000) // (see tp_versions_used)
Expand Down Expand Up @@ -291,6 +297,7 @@ typedef struct _heaptypeobject {
PyAPI_FUNC(const char *) _PyType_Name(PyTypeObject *);
PyAPI_FUNC(PyObject *) _PyType_Lookup(PyTypeObject *, PyObject *);
PyAPI_FUNC(PyObject *) _PyType_LookupRef(PyTypeObject *, PyObject *);
PyAPI_FUNC(int) _PyType_HasExtensionSlots(PyTypeObject *);
PyAPI_FUNC(PyObject *) PyType_GetDict(PyTypeObject *);

PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int);
Expand Down
5 changes: 5 additions & 0 deletions Include/cpython/weakrefobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ struct _PyWeakReference {

/* A callable to invoke when wr_object dies, or NULL if none. */
PyObject *wr_callback;
/* ID of the interpreter where the callback resides.
* Used for immutable objects to know which interpreter to call back into.
* This is -1 if the callback is NULL.
*/
int64_t callback_ipid;

/* A cache for wr_object's hash code. As usual for hashes, this is -1
* if the hash code isn't known yet.
Expand Down
1 change: 1 addition & 0 deletions Include/descrobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ struct PyMemberDef {
#define Py_AUDIT_READ 2 // Added in 3.10, harmless no-op before that
#define _Py_WRITE_RESTRICTED 4 // Deprecated, no-op. Do not reuse the value.
#define Py_RELATIVE_OFFSET 8
// TODO(Immutable): Could use this to mark members as needing a lock.

PyAPI_FUNC(PyObject *) PyMember_GetOne(const char *, PyMemberDef *);
PyAPI_FUNC(int) PyMember_SetOne(char *, PyMemberDef *, PyObject *);
Expand Down
3 changes: 2 additions & 1 deletion Include/dictobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ PyAPI_FUNC(PyObject *) PyDict_GetItem(PyObject *mp, PyObject *key);
PyAPI_FUNC(PyObject *) PyDict_GetItemWithError(PyObject *mp, PyObject *key);
PyAPI_FUNC(int) PyDict_SetItem(PyObject *mp, PyObject *key, PyObject *item);
PyAPI_FUNC(int) PyDict_DelItem(PyObject *mp, PyObject *key);
PyAPI_FUNC(void) PyDict_Clear(PyObject *mp);
// Note(Immutable): If dictionary is immutable, then clear can fail. Had to change signature here!
PyAPI_FUNC(int) PyDict_Clear(PyObject *mp);
PyAPI_FUNC(int) PyDict_Next(
PyObject *mp, Py_ssize_t *pos, PyObject **key, PyObject **value);
PyAPI_FUNC(PyObject *) PyDict_Keys(PyObject *mp);
Expand Down
19 changes: 19 additions & 0 deletions Include/immutability.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef Py_IMMUTABILITY_H
#define Py_IMMUTABILITY_H

#ifdef __cplusplus
extern "C" {
#endif


#ifndef Py_LIMITED_API
# define Py_CPYTHON_IMMUTABLE_H
# include "cpython/immutability.h"
# undef Py_CPYTHON_IMMUTABLE_H
#endif


#ifdef __cplusplus
}
#endif
#endif /* !Py_IMMUTABILITY_H */
21 changes: 15 additions & 6 deletions Include/internal/pycore_cell.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,30 @@ extern "C" {
// Sets the cell contents to `value` and return previous contents. Steals a
// reference to `value`.
static inline PyObject *
PyCell_SwapTakeRef(PyCellObject *cell, PyObject *value)
PyCell_SwapTakeRef(PyCellObject *cell, PyObject *value, int* result)
{
PyObject *old_value;
PyObject *old_value = NULL;
*result = 0;
Py_BEGIN_CRITICAL_SECTION(cell);
old_value = cell->ob_ref;
FT_ATOMIC_STORE_PTR_RELEASE(cell->ob_ref, value);
if(Py_CHECKWRITE(cell)){
old_value = cell->ob_ref;
FT_ATOMIC_STORE_PTR_RELEASE(cell->ob_ref, value);
}
else {
*result = -1;
Py_XDECREF(value);
}
Py_END_CRITICAL_SECTION();
return old_value;
}

static inline void
static inline int
PyCell_SetTakeRef(PyCellObject *cell, PyObject *value)
{
PyObject *old_value = PyCell_SwapTakeRef(cell, value);
int result = 0;
PyObject *old_value = PyCell_SwapTakeRef(cell, value, &result);
Py_XDECREF(old_value);
return result;
}

// Gets the cell contents. Returns a new reference.
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ PyAPI_FUNC(int) _PyEval_ExceptionGroupMatch(_PyInterpreterFrame *, PyObject* exc
PyAPI_FUNC(void) _PyEval_FormatAwaitableError(PyThreadState *tstate, PyTypeObject *type, int oparg);
PyAPI_FUNC(void) _PyEval_FormatExcCheckArg(PyThreadState *tstate, PyObject *exc, const char *format_str, PyObject *obj);
PyAPI_FUNC(void) _PyEval_FormatExcUnbound(PyThreadState *tstate, PyCodeObject *co, int oparg);
PyAPI_FUNC(void) _PyEval_FormatExcNotWriteable(PyThreadState *tstate, PyCodeObject *co, int oparg);
PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs);
PyAPI_FUNC(PyObject *) _PyEval_ImportFrom(PyThreadState *, PyObject *, PyObject *);
PyAPI_FUNC(PyObject *) _PyEval_ImportName(PyThreadState *, _PyInterpreterFrame *, PyObject *, PyObject *, PyObject *);
Expand Down
48 changes: 26 additions & 22 deletions Include/internal/pycore_freelist_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,33 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#define MAXFREELIST(x) x
//#define MAXFREELIST(x) 0


# define PyTuple_MAXSAVESIZE 20 // Largest tuple to save on freelist
# define Py_tuple_MAXFREELIST 2000 // Maximum number of tuples of each size to save
# define Py_lists_MAXFREELIST 80
# define Py_list_iters_MAXFREELIST 10
# define Py_tuple_iters_MAXFREELIST 10
# define Py_dicts_MAXFREELIST 80
# define Py_dictkeys_MAXFREELIST 80
# define Py_floats_MAXFREELIST 100
# define Py_complexes_MAXFREELIST 100
# define Py_ints_MAXFREELIST 100
# define Py_slices_MAXFREELIST 1
# define Py_ranges_MAXFREELIST 6
# define Py_range_iters_MAXFREELIST 6
# define Py_contexts_MAXFREELIST 255
# define Py_async_gens_MAXFREELIST 80
# define Py_async_gen_asends_MAXFREELIST 80
# define Py_futureiters_MAXFREELIST 255
# define Py_object_stack_chunks_MAXFREELIST 4
# define Py_unicode_writers_MAXFREELIST 1
# define Py_bytes_writers_MAXFREELIST 1
# define Py_pycfunctionobject_MAXFREELIST 16
# define Py_pycmethodobject_MAXFREELIST 16
# define Py_pymethodobjects_MAXFREELIST 20
# define Py_tuple_MAXFREELIST MAXFREELIST(2000) // Maximum number of tuples of each size to save
# define Py_lists_MAXFREELIST MAXFREELIST(80)
# define Py_list_iters_MAXFREELIST MAXFREELIST(10)
# define Py_tuple_iters_MAXFREELIST MAXFREELIST(10)
# define Py_dicts_MAXFREELIST MAXFREELIST(80)
# define Py_dictkeys_MAXFREELIST MAXFREELIST(80)
# define Py_floats_MAXFREELIST MAXFREELIST(100)
# define Py_complexes_MAXFREELIST MAXFREELIST(100)
# define Py_ints_MAXFREELIST MAXFREELIST(100)
# define Py_slices_MAXFREELIST MAXFREELIST(1)
# define Py_ranges_MAXFREELIST MAXFREELIST(6)
# define Py_range_iters_MAXFREELIST MAXFREELIST(6)
# define Py_contexts_MAXFREELIST MAXFREELIST(255)
# define Py_async_gens_MAXFREELIST MAXFREELIST(80)
# define Py_async_gen_asends_MAXFREELIST MAXFREELIST(80)
# define Py_futureiters_MAXFREELIST MAXFREELIST(255)
# define Py_object_stack_chunks_MAXFREELIST MAXFREELIST(4)
# define Py_unicode_writers_MAXFREELIST MAXFREELIST(1)
# define Py_bytes_writers_MAXFREELIST MAXFREELIST(1)
# define Py_pycfunctionobject_MAXFREELIST MAXFREELIST(16)
# define Py_pycmethodobject_MAXFREELIST MAXFREELIST(16)
# define Py_pymethodobjects_MAXFREELIST MAXFREELIST(20)

// A generic freelist of either PyObjects or other data structures.
struct _Py_freelist {
Expand Down
10 changes: 10 additions & 0 deletions Include/internal/pycore_gc.h
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ static inline void _PyGC_CLEAR_FINALIZED(PyObject *op) {
#endif
}

static inline void _PyGC_CLEAR_COLLECTING(PyObject *op) {
#ifdef Py_GIL_DISABLED
// TODO(immutable): Does NoGil have a collecting flag? If so, how do we
// clear it?
#else
PyGC_Head *gc = _Py_AS_GC(op);
gc->_gc_prev &= ~_PyGC_PREV_MASK_COLLECTING;
#endif
}


/* Tell the GC to track this object.
*
Expand Down
Loading
Loading