From a0c4e23639a545c4940a34e0e4e299f7ccd7c3dd Mon Sep 17 00:00:00 2001 From: CaQtimlThinkPad Date: Thu, 29 Jan 2026 14:25:04 +0100 Subject: [PATCH 01/37] add script for cleaning old cpython installation after merging updated cpython from mjp41 --- clean_old_cpython.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 clean_old_cpython.sh diff --git a/clean_old_cpython.sh b/clean_old_cpython.sh new file mode 100644 index 00000000000000..840c1492dd5479 --- /dev/null +++ b/clean_old_cpython.sh @@ -0,0 +1,3 @@ +make distclean # Clears out the old configuration and object files +./configure --with-pydebug --with-assertions CFLAGS='-O0 -g' CXXFLAGS='-O0 -g' +make -j8 From e5949a956df169affc72e94bd8d262384bca48fc Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Wed, 18 Feb 2026 13:17:05 +0100 Subject: [PATCH 02/37] Add support to "range" object - version 1 (incomplete) --- Objects/abstract.c | 4 ++- Objects/rangeobject.c | 76 ++++++++++++++++++++++++++++++++++++++++--- test_code/test1_1.py | 21 ++++++++++++ test_code/test1_2.py | 25 ++++++++++++++ test_code/test1_3.py | 28 ++++++++++++++++ test_code/test1_4.py | 35 ++++++++++++++++++++ 6 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 test_code/test1_1.py create mode 100644 test_code/test1_2.py create mode 100644 test_code/test1_3.py create mode 100644 test_code/test1_4.py diff --git a/Objects/abstract.c b/Objects/abstract.c index ca3d013593388e..a21d703017593a 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -1466,7 +1466,8 @@ _PyNumber_Index(PyObject *item) } if (PyLong_Check(item)) { - return Py_NewRef(item); + // PyRegion_AddLocalRef(item); // Can Fail + return PyRegion_NewRef(item); } if (!_PyIndex_Check(item)) { PyErr_Format(PyExc_TypeError, @@ -1508,6 +1509,7 @@ PyObject * PyNumber_Index(PyObject *item) { PyObject *result = _PyNumber_Index(item); + // For subclass handling if (result != NULL && !PyLong_CheckExact(result)) { Py_SETREF(result, _PyLong_Copy((PyLongObject *)result)); } diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index a4b170c368747d..7ee8495c64a4b1 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -39,6 +39,7 @@ validate_step(PyObject *step) if (step && _PyLong_IsZero((PyLongObject *)step)) { PyErr_SetString(PyExc_ValueError, "range() arg 3 must not be zero"); + PyRegion_RemoveLocalRef(step); Py_CLEAR(step); } @@ -65,6 +66,14 @@ make_range_object(PyTypeObject *type, PyObject *start, return NULL; } } + if(PyRegion_TakeRefs(obj, start, stop, step, length)) + { + Py_DECREF(obj); + PyRegion_RemoveLocalRef(length); + Py_DECREF(length); + return NULL; + } + obj->start = start; obj->stop = stop; obj->step = step; @@ -82,31 +91,50 @@ range_from_array(PyTypeObject *type, PyObject *const *args, Py_ssize_t num_args) { rangeobject *obj; PyObject *start = NULL, *stop = NULL, *step = NULL; + // PyObject *startLog = NULL, *stopLog = NULL; // For Debug extra +1 of "step" switch (num_args) { case 3: step = args[2]; + // start = args[0]; // For Debug extra +1 of "step" + // stop = args[1]; // For Debug extra +1 of "step" _Py_FALLTHROUGH; case 2: /* Convert borrowed refs to owned refs */ start = PyNumber_Index(args[0]); + // if (start == NULL || stop == NULL) { + // start = args[0]; // For Debug extra +1 of "step" + // stop = args[1]; // For Debug extra +1 of "step" + // } + // start = PyNumber_Index(start); // For Debug extra +1 of "step" if (!start) { return NULL; } stop = PyNumber_Index(args[1]); + // stop = PyNumber_Index(stop); // For Debug extra +1 of "step" if (!stop) { + PyRegion_RemoveLocalRef(start); // Since start has already increased the _lrc but stop failed to convert, we need to remove the local ref for start to avoid refcount issues. + // RemoveLocalRef before Py_DECREF to prevent the object to be deallocated before we remove the local ref, which can cause refcount issues. Py_DECREF(start); return NULL; } - step = validate_step(step); /* Caution, this can clear exceptions */ + step = validate_step(step); /* Caution, this can clear exceptions */ + /* Also have Py_INCREF inside in the form of Py_NEWREF*/ + // validate_step already handles the case that no step is provided, so we don't need to handle that case here. + // This if-statement handles only the case that step is provided but invalid (e.g. step is 0 or step is not an integer). if (!step) { + PyRegion_RemoveLocalRef(start); // Since start has already increased the _lrc but step failed to convert, we need to remove the local ref for start to avoid refcount issues. + PyRegion_RemoveLocalRef(stop); // Since stop has already increased the _lrc but step failed to convert, we need to remove the local ref for stop to avoid refcount issues. Py_DECREF(start); Py_DECREF(stop); return NULL; } break; case 1: + // printf("num_args is 1\n"); stop = PyNumber_Index(args[0]); + // stop = args[0]; // For Debug extra +1 of "step" + // stop = PyNumber_Index(stop); // For Debug extra +1 of "step" if (!stop) { return NULL; } @@ -170,9 +198,14 @@ static void range_dealloc(PyObject *op) { rangeobject *r = (rangeobject*)op; + + PyRegion_RemoveLocalRef(r->start); + PyRegion_RemoveLocalRef(r->stop); + PyRegion_RemoveLocalRef(r->step); Py_DECREF(r->start); Py_DECREF(r->stop); Py_DECREF(r->step); + // TODO: Check if PyRegion_RemoveLocalRef(r->length) is required here. Py_DECREF(r->length); _Py_FREELIST_FREE_OBJ(ranges, r, PyObject_Free); } @@ -778,6 +811,21 @@ static PyMemberDef range_members[] = { {0} }; +static int +range_traverse(PyObject *self, visitproc visit, void *arg) +{ + rangeobject *r = (rangeobject *)self; + + // Py_VISIT is a macro that calls visit(object, arg) + // and returns early if visit() returns non-zero (error) + Py_VISIT(r->start); + Py_VISIT(r->stop); + Py_VISIT(r->step); + Py_VISIT(r->length); + + return 0; +} + PyTypeObject PyRange_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "range", /* Name of this type */ @@ -800,7 +848,7 @@ PyTypeObject PyRange_Type = { 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_SEQUENCE, /* tp_flags */ range_doc, /* tp_doc */ - 0, /* tp_traverse */ + range_traverse, /* tp_traverse */ 0, /* tp_clear */ range_richcompare, /* tp_richcompare */ 0, /* tp_weaklistoffset */ @@ -904,6 +952,14 @@ rangeiter_dealloc(PyObject *self) _Py_FREELIST_FREE_OBJ(range_iters, (_PyRangeIterObject *)self, PyObject_Free); } +static int +rangeiter_traverse(PyObject *self, visitproc visit, void *arg) +{ + // _PyRangeIterObject holds only C longs (start, step, len), + // not PyObject* references, so there is nothing to visit. + return 0; +} + PyDoc_STRVAR(reduce_doc, "Return state information for pickling."); PyDoc_STRVAR(setstate_doc, "Set state information for unpickling."); @@ -937,7 +993,7 @@ PyTypeObject PyRangeIter_Type = { 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT, /* tp_flags */ 0, /* tp_doc */ - 0, /* tp_traverse */ + rangeiter_traverse, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ @@ -1118,6 +1174,18 @@ longrangeiter_next(PyObject *op) return result; } +static int +longrangeiter_traverse(PyObject *self, visitproc visit, void *arg) +{ + longrangeiterobject *r = (longrangeiterobject *)self; + + Py_VISIT(r->start); + Py_VISIT(r->step); + Py_VISIT(r->len); + + return 0; +} + PyTypeObject PyLongRangeIter_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "longrange_iterator", /* tp_name */ @@ -1141,7 +1209,7 @@ PyTypeObject PyLongRangeIter_Type = { 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT, /* tp_flags */ 0, /* tp_doc */ - 0, /* tp_traverse */ + longrangeiter_traverse, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ diff --git a/test_code/test1_1.py b/test_code/test1_1.py new file mode 100644 index 00000000000000..ff5bfd820c1699 --- /dev/null +++ b/test_code/test1_1.py @@ -0,0 +1,21 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r = Region() +# freeze(10) +r.stop = 44543543543553534554 +print("sys.getrefcount(r.stop): ", sys.getrefcount(r.stop)) +print(f"{r.owns(r.stop)}") + +print(f"Initial Region: {r}") +#print(sys.getrefcount(r.stop)) +# print(f"{r.stop}") +ra = range(r.stop) +print(f"{ra}") +print(f"{r.owns(ra)}, {is_local(ra)}") +# print("sys.getrefcount(r.stop): ", sys.getrefcount(r.stop)) +#print(sys.getrefcount(r.stop)) +print(f"Region after setting stop: {r}") +ra = None +print(f"Region after setting ra to None: {r}") \ No newline at end of file diff --git a/test_code/test1_2.py b/test_code/test1_2.py new file mode 100644 index 00000000000000..693cf61f9474be --- /dev/null +++ b/test_code/test1_2.py @@ -0,0 +1,25 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r = Region() +# freeze(10) +r.start = 44543543543553534000 +# r.start=3 +r.stop = 44543543543553534554 +print("sys.getrefcount(r.start): ", sys.getrefcount(r.start)) +print("sys.getrefcount(r.stop): ", sys.getrefcount(r.stop)) +print(f"{r.owns(r.start)}") +print(f"{r.owns(r.stop)}") + +print(f"Initial Region: {r}") +#print(sys.getrefcount(r.stop)) +# print(f"{r.stop}") +ra = range(r.start, r.stop) +print(f"{ra}") +print(f"{r.owns(ra)}, {is_local(ra)}") +# print("sys.getrefcount(r.stop): ", sys.getrefcount(r.stop)) +#print(sys.getrefcount(r.stop)) +print(f"Region after setting stop: {r}") +ra = None +print(f"Region after setting ra to None: {r}") \ No newline at end of file diff --git a/test_code/test1_3.py b/test_code/test1_3.py new file mode 100644 index 00000000000000..e20a99623c84c0 --- /dev/null +++ b/test_code/test1_3.py @@ -0,0 +1,28 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r = Region() +# freeze(10) +r.start = 44543543543553534000 +# r.start=3 +r.stop = 44543543543553534554 +r.step = 44543543543555 +print("sys.getrefcount(r.start): ", sys.getrefcount(r.start)) +print("sys.getrefcount(r.stop): ", sys.getrefcount(r.stop)) +print("sys.getrefcount(r.step): ", sys.getrefcount(r.step)) +print(f"{r.owns(r.start)}") +print(f"{r.owns(r.stop)}") +print(f"{r.owns(r.step)}") + +print(f"Initial Region: {r}") +#print(sys.getrefcount(r.stop)) +# print(f"{r.stop}") +ra = range(r.start, r.stop, r.step) +print(f"{ra}") +print(f"{r.owns(ra)}, {is_local(ra)}") +# print("sys.getrefcount(r.stop): ", sys.getrefcount(r.stop)) +#print(sys.getrefcount(r.stop)) +print(f"Region after setting stop: {r}") +ra = None +print(f"Region after setting ra to None: {r}") \ No newline at end of file diff --git a/test_code/test1_4.py b/test_code/test1_4.py new file mode 100644 index 00000000000000..4885e7c91bfb91 --- /dev/null +++ b/test_code/test1_4.py @@ -0,0 +1,35 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r1 = Region() +r2 = Region() +r3 = Region() +# freeze(10) +r1.start = 445435435435535340000000 +r2.stop = 445435435435535345540000 +r3.step = 4454354354355500000 +# r1.start = 1 +# r2.stop = 10 +# r3.step = 2 +# print("sys.getrefcount(r.start): ", sys.getrefcount(r1.start)) +# print("sys.getrefcount(r.stop): ", sys.getrefcount(r2.stop)) +# print("sys.getrefcount(r.step): ", sys.getrefcount(r3.step)) +# print(f"{r1.owns(r1.start)}") +# print(f"{r2.owns(r2.stop)}") +# print(f"{r3.owns(r3.step)}") + +print(f"Initial Region: {r1}") +print(f"Initial Region: {r2}") +print(f"Initial Region: {r3}") +#print(sys.getrefcount(r.stop)) +# print(f"{r.stop}") +ra = range(r1.start, r2.stop, r3.step) +print(f"{ra}") +print(f"Region 1 after setting stop: {r1}") +print(f"Region 2 after setting stop: {r2}") +print(f"Region 3 after setting step: {r3}") +ra = None +print(f"Region 1 after deleting ra: {r1}") +print(f"Region 2 after deleting ra: {r2}") +print(f"Region 3 after deleting ra: {r3}") \ No newline at end of file From 5ee2b63eb37a90a3fef2efba751d6a0e9c16e292 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Thu, 19 Feb 2026 14:48:28 +0100 Subject: [PATCH 03/37] update to support tp_new, tp_vectorcall, tp_dealloc, and range_repr for "range". Should test with test1_4.py --- Objects/abstract.c | 1 + Objects/rangeobject.c | 44 ++++++------ test_code/report_code/closed_region.py | 11 +++ test_code/report_code/freeze_item.py | 14 ++++ .../report_code/move_from_localR_to_R.py | 14 ++++ test_code/report_code/race_cond.py | 72 +++++++++++++++++++ test_code/test1_4.py | 1 + test_code/test1_5.py | 31 ++++++++ 8 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 test_code/report_code/closed_region.py create mode 100644 test_code/report_code/freeze_item.py create mode 100644 test_code/report_code/move_from_localR_to_R.py create mode 100644 test_code/report_code/race_cond.py create mode 100644 test_code/test1_5.py diff --git a/Objects/abstract.c b/Objects/abstract.c index a21d703017593a..6a0b7dd93bd92d 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -1564,6 +1564,7 @@ PyNumber_AsSsize_t(PyObject *item, PyObject *err) } finish: + PyRegion_RemoveLocalRef(value); Py_DECREF(value); return result; } diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 7ee8495c64a4b1..8d81b95c008a96 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -62,12 +62,14 @@ make_range_object(PyTypeObject *type, PyObject *start, if (obj == NULL) { obj = PyObject_New(rangeobject, type); if (obj == NULL) { + PyRegion_RemoveLocalRef(length); Py_DECREF(length); return NULL; } } if(PyRegion_TakeRefs(obj, start, stop, step, length)) { + assert(PyRegion_IsLocal(obj)); // No write barrier bc obj is newly created and being in the local region Py_DECREF(obj); PyRegion_RemoveLocalRef(length); Py_DECREF(length); @@ -96,22 +98,14 @@ range_from_array(PyTypeObject *type, PyObject *const *args, Py_ssize_t num_args) switch (num_args) { case 3: step = args[2]; - // start = args[0]; // For Debug extra +1 of "step" - // stop = args[1]; // For Debug extra +1 of "step" _Py_FALLTHROUGH; case 2: /* Convert borrowed refs to owned refs */ start = PyNumber_Index(args[0]); - // if (start == NULL || stop == NULL) { - // start = args[0]; // For Debug extra +1 of "step" - // stop = args[1]; // For Debug extra +1 of "step" - // } - // start = PyNumber_Index(start); // For Debug extra +1 of "step" if (!start) { return NULL; } stop = PyNumber_Index(args[1]); - // stop = PyNumber_Index(stop); // For Debug extra +1 of "step" if (!stop) { PyRegion_RemoveLocalRef(start); // Since start has already increased the _lrc but stop failed to convert, we need to remove the local ref for start to avoid refcount issues. // RemoveLocalRef before Py_DECREF to prevent the object to be deallocated before we remove the local ref, which can cause refcount issues. @@ -131,10 +125,7 @@ range_from_array(PyTypeObject *type, PyObject *const *args, Py_ssize_t num_args) } break; case 1: - // printf("num_args is 1\n"); stop = PyNumber_Index(args[0]); - // stop = args[0]; // For Debug extra +1 of "step" - // stop = PyNumber_Index(stop); // For Debug extra +1 of "step" if (!stop) { return NULL; } @@ -157,9 +148,15 @@ range_from_array(PyTypeObject *type, PyObject *const *args, Py_ssize_t num_args) } /* Failed to create object, release attributes */ + PyRegion_RemoveLocalRef(start); + PyRegion_RemoveLocalRef(stop); + PyRegion_RemoveLocalRef(step); Py_DECREF(start); Py_DECREF(stop); Py_DECREF(step); + // PyRegion_CLEARLOCAL(start); + // PyRegion_CLEARLOCAL(stop); + // PyRegion_CLEARLOCAL(step); return NULL; } @@ -198,15 +195,14 @@ static void range_dealloc(PyObject *op) { rangeobject *r = (rangeobject*)op; - - PyRegion_RemoveLocalRef(r->start); - PyRegion_RemoveLocalRef(r->stop); - PyRegion_RemoveLocalRef(r->step); - Py_DECREF(r->start); - Py_DECREF(r->stop); - Py_DECREF(r->step); - // TODO: Check if PyRegion_RemoveLocalRef(r->length) is required here. - Py_DECREF(r->length); + PyRegion_CLEAR(r, r->start); + /* Equivalent to: + PyRegion_RemoveRef(r, r->start); + Py_DECREF(r->start); + */ + PyRegion_CLEAR(r, r->stop); + PyRegion_CLEAR(r, r->step); + PyRegion_CLEAR(r, r->length); _Py_FREELIST_FREE_OBJ(ranges, r, PyObject_Free); } @@ -295,6 +291,10 @@ compute_range_length(PyObject *start, PyObject *stop, PyObject *step) if (cmp_result == 1) { lo = start; hi = stop; + if(PyRegion_AddLocalRef(step)) // Should never fail, but just in case + { + return NULL; + } Py_INCREF(step); } else { lo = stop; @@ -307,6 +307,7 @@ compute_range_length(PyObject *start, PyObject *stop, PyObject *step) /* if (lo >= hi), return length of 0. */ cmp_result = PyObject_RichCompareBool(lo, hi, Py_GE); if (cmp_result != 0) { + PyRegion_RemoveLocalRef(step); Py_DECREF(step); if (cmp_result < 0) return NULL; @@ -328,11 +329,13 @@ compute_range_length(PyObject *start, PyObject *stop, PyObject *step) Py_DECREF(tmp2); Py_DECREF(diff); + PyRegion_RemoveLocalRef(step); Py_DECREF(step); Py_DECREF(tmp1); return result; Fail: + PyRegion_RemoveLocalRef(step); Py_DECREF(step); Py_XDECREF(tmp2); Py_XDECREF(diff); @@ -838,6 +841,7 @@ PyTypeObject PyRange_Type = { 0, /* tp_as_async */ range_repr, /* tp_repr */ &range_as_number, /* tp_as_number */ + // TODO &range_as_sequence, /* tp_as_sequence */ &range_as_mapping, /* tp_as_mapping */ range_hash, /* tp_hash */ diff --git a/test_code/report_code/closed_region.py b/test_code/report_code/closed_region.py new file mode 100644 index 00000000000000..b46c68e7c3bc1b --- /dev/null +++ b/test_code/report_code/closed_region.py @@ -0,0 +1,11 @@ +from regions import Region, is_local, Cown + +r = Region() +obj = {"key": "value"} +r.start = obj +c = Cown(r) +c.acquire() +print(f"{c.value.start['key']}") +r = None +obj = None +c.release() \ No newline at end of file diff --git a/test_code/report_code/freeze_item.py b/test_code/report_code/freeze_item.py new file mode 100644 index 00000000000000..dc2aa4b863fefc --- /dev/null +++ b/test_code/report_code/freeze_item.py @@ -0,0 +1,14 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +obj = {"key": "value"} +print(f"Initial Region: {r}") +r.start = obj +print(f"Region after setting start: {r}") +print(f"Obj is in the local region: {is_local(obj)}") +print(f"Region owns obj: {r.owns(obj)}") +freeze(obj) +print(f"Region after freezing obj: {r}") +print(f"Obj is in the local region: {is_local(obj)}") +print(f"Region owns obj: {r.owns(obj)}") \ No newline at end of file diff --git a/test_code/report_code/move_from_localR_to_R.py b/test_code/report_code/move_from_localR_to_R.py new file mode 100644 index 00000000000000..4c03706d6b95a0 --- /dev/null +++ b/test_code/report_code/move_from_localR_to_R.py @@ -0,0 +1,14 @@ +from regions import Region, is_local + +r = Region() +child_obj = {"child_key": "child_value"} +obj = {"key": "value", "child": child_obj} +print(f"Initial Region: {r}") +print(f"Obj is in the local region: {is_local(obj)}") # True +print(f"Child obj is in the local region: {is_local(child_obj)}") # True +r.start = obj +print(f"Region after setting start: {r}") +print(f"Obj is in the local region: {is_local(obj)}") # False +print(f"Child obj is in the local region: {is_local(child_obj)}") # False +print(f"Region owns obj: {r.owns(obj)}") # True +print(f"Region owns child obj: {r.owns(child_obj)}") # True \ No newline at end of file diff --git a/test_code/report_code/race_cond.py b/test_code/report_code/race_cond.py new file mode 100644 index 00000000000000..b6777a100297b8 --- /dev/null +++ b/test_code/report_code/race_cond.py @@ -0,0 +1,72 @@ +from regions import Region, is_local, Cown +import threading + +r = Region() +# s_arr = np.empty(3, dtype=object) # Array that holds Python objects +s_arr = list(range(3)) # Using a list instead of numpy array to avoid complications with numpy's internal optimizations +r.arr = s_arr +c = Cown(r) +# ask for the diagram drawing again from Fred +print(f"r.owns(s_arr): {r.owns(s_arr)}") # True +# Create a local Python object +local_dict = {"key": "value", "count": 0} + +# Assign it to the array +s_arr[0] = local_dict # Internally writes PyObject* into data buffer + +# WITHOUT BARRIER: +# Problem: local_dict is still local, but now referenced from arr inside region r +print(f"s_arr in region: {r.owns(s_arr)}") # True +print(f"local_dict in region: {r.owns(local_dict)}") # False - still local +print(f"local_dict is local: {is_local(local_dict)}") # True - didn't transfer! - PROBLEM! + +print(f"Region r: {str(r)}") + +# DANGER: Array contains reference to local object +# If another thread acquires Cown and modifies arr[0], +# but we still have local_dict reference - DATA RACE! + +# c = Cown(r) + +# print(f"{c.value.arr[0]}") +# c.release() + +print("Starting threads that will cause race condition despite using Cown...") + +def race_condition_despite_using_cown(): + c.acquire() + r = c.value + for _ in range(5000): + # print(f"c.owned: {c.owned()}, c.owned_by_thread(); {c.owned_by_thread()}") + # 1. READ + val = r.arr[0]["count"] # work because val takes the copy of the PythonObject of number. + # if "val" is an actual object, val=None is also required because it still makes the region opens (like r). + + # 2. DELAY (This forces the other thread to read the same old 'val') + # Even a tiny computation creates the window for a race condition + _ = [i**2 for i in range(10)] + + # 3. WRITE + r.arr[0]["count"] = val + 1 + r = None + c.release() + +# Launch two threads to increment the same spot +t1 = threading.Thread(target=race_condition_despite_using_cown) +t2 = threading.Thread(target=race_condition_despite_using_cown) + +t1.start() +t2.start() + +r = None +s_arr = None +c.release() + +t1.join() +t2.join() + +c.acquire() #needed to print the data in the print statements, otherwise c.value is not accessible + +print(f"Result: {c.value.arr[0]['count']}") +print(f"Expected: 10000") +print(f"Difference: {10000 - c.value.arr[0]['count']} (If this is > 0, you found the race!)") \ No newline at end of file diff --git a/test_code/test1_4.py b/test_code/test1_4.py index 4885e7c91bfb91..c97528a7ad88d5 100644 --- a/test_code/test1_4.py +++ b/test_code/test1_4.py @@ -24,6 +24,7 @@ print(f"Initial Region: {r3}") #print(sys.getrefcount(r.stop)) # print(f"{r.stop}") +input("Continue") ra = range(r1.start, r2.stop, r3.step) print(f"{ra}") print(f"Region 1 after setting stop: {r1}") diff --git a/test_code/test1_5.py b/test_code/test1_5.py new file mode 100644 index 00000000000000..b92aeb3709b12c --- /dev/null +++ b/test_code/test1_5.py @@ -0,0 +1,31 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r1 = Region() +r2 = Region() +r3 = Region() +# freeze(10) +r1.start = 445435435435535340000000 +r2.stop = 445435435435535345547898 +r3.step = 445435435435550 +# r1.start = 1 +# r2.stop = 10 +# r3.step = 2 +# print("sys.getrefcount(r.start): ", sys.getrefcount(r1.start)) +# print("sys.getrefcount(r.stop): ", sys.getrefcount(r2.stop)) +# print("sys.getrefcount(r.step): ", sys.getrefcount(r3.step)) +# print(f"{r1.owns(r1.start)}") +# print(f"{r2.owns(r2.stop)}") +# print(f"{r3.owns(r3.step)}") + +print(f"Initial Region: {r1}") +print(f"Initial Region: {r2}") +print(f"Initial Region: {r3}") +#print(sys.getrefcount(r.stop)) +# print(f"{r.stop}") +ra1 = range(r1.start, r2.stop, r3.step)[2] + +print(f"{r1.owns(ra1)}") +print(f"{r2.owns(ra1)}") +print(f"{r3.owns(ra1)}") \ No newline at end of file From c7fa2c6a41f139c1af15a0e9ee02fff6d9e6cffd Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Thu, 19 Feb 2026 22:47:21 +0100 Subject: [PATCH 04/37] add barrier for iter(range) and reverse(range) with the support to dealloc --- Objects/rangeobject.c | 19 ++++++++++++---- test_code/longrangeiter_1.py | 27 ++++++++++++++++++++++ test_code/longrangeiter_2.py | 30 +++++++++++++++++++++++++ test_code/rangeiter_vs_longrangeiter.py | 11 +++++++++ test_code/test1_3.py | 6 +++++ test_code/test1_4.py | 6 ++--- test_code/test1_5.py | 11 +++++---- 7 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 test_code/longrangeiter_1.py create mode 100644 test_code/longrangeiter_2.py create mode 100644 test_code/rangeiter_vs_longrangeiter.py diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 8d81b95c008a96..6e186f24cad154 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -1150,9 +1150,12 @@ static void longrangeiter_dealloc(PyObject *op) { longrangeiterobject *r = (longrangeiterobject*)op; - Py_XDECREF(r->start); - Py_XDECREF(r->step); - Py_XDECREF(r->len); + PyRegion_CLEAR(r, r->start); + PyRegion_CLEAR(r, r->step); + PyRegion_CLEAR(r, r->len); + // Py_XDECREF(r->start); + // Py_XDECREF(r->step); + // Py_XDECREF(r->len); PyObject_Free(r); } @@ -1218,7 +1221,7 @@ PyTypeObject PyLongRangeIter_Type = { 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ PyObject_SelfIter, /* tp_iter */ - longrangeiter_next, /* tp_iternext */ + longrangeiter_next, /* tp_iternext */ /*next(it)*/ longrangeiter_methods, /* tp_methods */ 0, }; @@ -1272,6 +1275,10 @@ range_iter(PyObject *seq) if (it == NULL) return NULL; + if(PyRegion_AddRefs(it, r->start, r->step, r->length)) { + Py_DECREF(it); + return NULL; + } it->start = Py_NewRef(r->start); it->step = Py_NewRef(r->step); it->len = Py_NewRef(r->length); @@ -1355,6 +1362,10 @@ range_reverse(PyObject *seq, PyObject *Py_UNUSED(ignored)) it->start = it->step = NULL; /* start + (len - 1) * step */ + if(PyRegion_AddRef(it, range->length)) { + Py_DECREF(it); + return NULL; + } it->len = Py_NewRef(range->length); diff = PyNumber_Subtract(it->len, _PyLong_GetOne()); diff --git a/test_code/longrangeiter_1.py b/test_code/longrangeiter_1.py new file mode 100644 index 00000000000000..bc1c717f423078 --- /dev/null +++ b/test_code/longrangeiter_1.py @@ -0,0 +1,27 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r1 = Region() +# input("0: Press Enter to continue...") +r1.start = 2**100 +r1.stop = 2**200 +r1.step = 2**50 +print(f"{sys.getrefcount(r1.start)}") +print(f"Initial Region: {r1}") #_lrc=3 +# input("1: Press Enter to continue...") +r_large = range(r1.start, r1.stop, r1.step) +print(f"{r_large}") +print(f"{r1.owns(r_large)}") +print(f"Region 1 after setting start: {r1}") #_lrc=6 +# input("2: Press Enter to continue...") +r1.range = r_large +print(f"Region 1 after moving range into the region: {r1}") #_lrc=4: -3 from subtracting start, stop, and end to the region since the range obj is moved into the region, +1 from r_large +print(f"{r1.owns(r_large)}") +input("3: Press Enter to continue...") +iter_range = iter(r_large) # Create "iter" object from "range" +print(f"Region 1 after creating iter: {r1}") # _lrc=6 since it borrows r.start and r.step +iter_range = None +print(f"Region 1 after deleting iter: {r1}") #_lrc=4 +r_large = None +print(f"Region 1 after deleting r_large: {r1}") #_lrc=3 \ No newline at end of file diff --git a/test_code/longrangeiter_2.py b/test_code/longrangeiter_2.py new file mode 100644 index 00000000000000..1d7ee9392ee2b7 --- /dev/null +++ b/test_code/longrangeiter_2.py @@ -0,0 +1,30 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r1 = Region() +# input("0: Press Enter to continue...") +r1.start = 2**100 +r1.stop = 2**200 +r1.step = 2**50 +print(f"{sys.getrefcount(r1.start)}") +print(f"Initial Region: {r1}") #_lrc=3 +# input("1: Press Enter to continue...") +r_large = range(r1.start, r1.stop, r1.step) +print(f"{r_large}") +print(f"{r1.owns(r_large)}") +print(f"Region 1 after setting start: {r1}") #_lrc=6 +# input("2: Press Enter to continue...") +r1.range = r_large +print(f"Region 1 after moving range into the region: {r1}") #_lrc=4: -3 from subtracting start, stop, and end to the region since the range obj is moved into the region, +1 from r_large +print(f"{r1.owns(r_large)}") +input("3: Press Enter to continue...") +reversed_range = reversed(r_large) # Create "iter" object from "range" +print(f"{reversed_range}") +# print(f"{hex(id(reversed_range)), hex(id(r_large))}") # reversed_range is not the same object as r_large. +print(f"Region 1 after creating reversed_range: {r1}") +input("continue") +reversed_range = None +print(f"Region 1 after deleting reversed_range: {r1}") +r_large = None +print(f"Region 1 after deleting r_large: {r1}") \ No newline at end of file diff --git a/test_code/rangeiter_vs_longrangeiter.py b/test_code/rangeiter_vs_longrangeiter.py new file mode 100644 index 00000000000000..66a3558ac3f221 --- /dev/null +++ b/test_code/rangeiter_vs_longrangeiter.py @@ -0,0 +1,11 @@ +# Small range - fits in C long +r_small = range(1, 100, 2) +it_small = iter(r_small) +print(type(r_small)) # +print(type(it_small)) # <- PyRangeIter_Type + +# Large range - values don't fit in C long +r_large = range(2**100, 2**200, 2**50) +it_large = iter(r_large) +print(type(r_large)) # <- still PyRange_Type! +print(type(it_large)) # <- PyLongRangeIter_Type \ No newline at end of file diff --git a/test_code/test1_3.py b/test_code/test1_3.py index e20a99623c84c0..2b35d6c2620feb 100644 --- a/test_code/test1_3.py +++ b/test_code/test1_3.py @@ -7,7 +7,12 @@ r.start = 44543543543553534000 # r.start=3 r.stop = 44543543543553534554 +# r.stop = 100 r.step = 44543543543555 +# r.step = 10 +# a=10**20 +# b=100000000000000000000 +# print(f"{hex(id(a))}, {hex(id(b))}") print("sys.getrefcount(r.start): ", sys.getrefcount(r.start)) print("sys.getrefcount(r.stop): ", sys.getrefcount(r.stop)) print("sys.getrefcount(r.step): ", sys.getrefcount(r.step)) @@ -18,6 +23,7 @@ print(f"Initial Region: {r}") #print(sys.getrefcount(r.stop)) # print(f"{r.stop}") +input("Press Enter to continue...") ra = range(r.start, r.stop, r.step) print(f"{ra}") print(f"{r.owns(ra)}, {is_local(ra)}") diff --git a/test_code/test1_4.py b/test_code/test1_4.py index c97528a7ad88d5..43222dd423d379 100644 --- a/test_code/test1_4.py +++ b/test_code/test1_4.py @@ -12,9 +12,9 @@ # r1.start = 1 # r2.stop = 10 # r3.step = 2 -# print("sys.getrefcount(r.start): ", sys.getrefcount(r1.start)) -# print("sys.getrefcount(r.stop): ", sys.getrefcount(r2.stop)) -# print("sys.getrefcount(r.step): ", sys.getrefcount(r3.step)) +print("sys.getrefcount(r1.start): ", sys.getrefcount(r1.start)) +print("sys.getrefcount(r2.stop): ", sys.getrefcount(r2.stop)) +print("sys.getrefcount(r3.step): ", sys.getrefcount(r3.step)) # print(f"{r1.owns(r1.start)}") # print(f"{r2.owns(r2.stop)}") # print(f"{r3.owns(r3.step)}") diff --git a/test_code/test1_5.py b/test_code/test1_5.py index b92aeb3709b12c..6003cc7c3dfb6d 100644 --- a/test_code/test1_5.py +++ b/test_code/test1_5.py @@ -7,8 +7,8 @@ r3 = Region() # freeze(10) r1.start = 445435435435535340000000 -r2.stop = 445435435435535345547898 -r3.step = 445435435435550 +r2.stop = 4454354354355353455478980000000 +r3.step = 44543543545555 # r1.start = 1 # r2.stop = 10 # r3.step = 2 @@ -24,8 +24,11 @@ print(f"Initial Region: {r3}") #print(sys.getrefcount(r.stop)) # print(f"{r.stop}") -ra1 = range(r1.start, r2.stop, r3.step)[2] +ra1 = range(r1.start, r2.stop, r3.step) print(f"{r1.owns(ra1)}") print(f"{r2.owns(ra1)}") -print(f"{r3.owns(ra1)}") \ No newline at end of file +print(f"{r3.owns(ra1)}") + +print(f"is_local(ra1[0]): {is_local(ra1[0])}") +print(f"is_local(ra1[1]): {is_local(ra1[1])}") \ No newline at end of file From 9c55a1117d3e26344431dbab85e540d5b2618888 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Sat, 21 Feb 2026 19:12:50 +0100 Subject: [PATCH 05/37] migrate range_as_mapping and decide that range_as_number and range_as_sequence do not need the migration. also migrate _PySlice_GetLongIndices, slice_dealloc, and slice_new since they are related to range_as_mapping --- Objects/abstract.c | 6 +++ Objects/object.c | 2 + Objects/rangeobject.c | 15 ++++++- Objects/sliceobject.c | 36 +++++++++++++++-- test_code/longrangeiter_1.py | 2 +- test_code/range_argumentIndex.py | 42 ++++++++++++++++++++ test_code/range_argumentSlicing.py | 38 ++++++++++++++++++ test_code/{test1_4.py => range_threeArgs.py} | 0 test_code/test1_5.py | 34 ---------------- 9 files changed, 135 insertions(+), 40 deletions(-) create mode 100644 test_code/range_argumentIndex.py create mode 100644 test_code/range_argumentSlicing.py rename test_code/{test1_4.py => range_threeArgs.py} (100%) delete mode 100644 test_code/test1_5.py diff --git a/Objects/abstract.c b/Objects/abstract.c index 6a0b7dd93bd92d..6f525cf2f4ff3d 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -1467,6 +1467,12 @@ _PyNumber_Index(PyObject *item) if (PyLong_Check(item)) { // PyRegion_AddLocalRef(item); // Can Fail + /* + if (PyRegion_AddLocalRef(item)) { + return NULL; + } + return Py_NewRef(item); + */ return PyRegion_NewRef(item); } if (!_PyIndex_Check(item)) { diff --git a/Objects/object.c b/Objects/object.c index 9e9abd5eee528e..291c697e7a5a2f 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1596,6 +1596,8 @@ _PyObject_GetDictPtr(PyObject *obj) PyObject * PyObject_SelfIter(PyObject *obj) { + // Don't need a write barrier since it returns itself. + // _lrc should be increased from the assignment if the iter is in the region return Py_NewRef(obj); } diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 6e186f24cad154..0e3060802647c7 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -423,6 +423,9 @@ compute_range_item(rangeobject *r, PyObject *arg) return result; } +/* +Different from range_subscript since range_item accepts only Py_ssize_t, while range_subscript accepts any PyObject as the index. +*/ static PyObject * range_item(PyObject *op, Py_ssize_t i) { @@ -451,14 +454,17 @@ compute_slice(rangeobject *r, PyObject *_slice) substep = PyNumber_Multiply(r->step, step); if (substep == NULL) goto fail; + PyRegion_RemoveRef(slice, step); Py_CLEAR(step); substart = compute_item(r, start); if (substart == NULL) goto fail; + PyRegion_RemoveRef(slice, start); Py_CLEAR(start); substop = compute_item(r, stop); if (substop == NULL) goto fail; + PyRegion_RemoveRef(slice, stop); Py_CLEAR(stop); result = make_range_object(Py_TYPE(r), substart, substop, substep); @@ -466,6 +472,9 @@ compute_slice(rangeobject *r, PyObject *_slice) return (PyObject *) result; } fail: + PyRegion_RemoveRef(slice, start); + PyRegion_RemoveRef(slice, stop); + PyRegion_RemoveRef(slice, step); Py_XDECREF(start); Py_XDECREF(stop); Py_XDECREF(step); @@ -706,6 +715,7 @@ range_index(PyObject *self, PyObject *ob) } static PySequenceMethods range_as_sequence = { + // No need for barrier for all of these since there is no modification to "rangeobject" range_length, /* sq_length */ 0, /* sq_concat */ 0, /* sq_repeat */ @@ -751,6 +761,8 @@ range_subscript(PyObject *op, PyObject *item) { rangeobject *self = (rangeobject*)op; if (_PyIndex_Check(item)) { + // No change needed even if the PyNumber_Index has PyRegion_NewRef. + // My guess: It does nothing if "item" is not in a region. PyObject *i, *result; i = PyNumber_Index(item); if (!i) @@ -760,6 +772,7 @@ range_subscript(PyObject *op, PyObject *item) return result; } if (PySlice_Check(item)) { + // TODO return compute_slice(self, item); } PyErr_Format(PyExc_TypeError, @@ -841,9 +854,9 @@ PyTypeObject PyRange_Type = { 0, /* tp_as_async */ range_repr, /* tp_repr */ &range_as_number, /* tp_as_number */ - // TODO &range_as_sequence, /* tp_as_sequence */ &range_as_mapping, /* tp_as_mapping */ + // TODO range_hash, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ diff --git a/Objects/sliceobject.c b/Objects/sliceobject.c index 17d1baaf22be4a..25f99ef5acf1b8 100644 --- a/Objects/sliceobject.c +++ b/Objects/sliceobject.c @@ -128,13 +128,17 @@ _PyBuildSlice_Consume2(PyObject *start, PyObject *stop, PyObject *step) } } + PyRegion_TakeRefs(obj, start, stop); obj->start = start; obj->stop = stop; + PyRegion_AddRef(obj, step); obj->step = Py_NewRef(step); _PyObject_GC_TRACK(obj); return obj; error: + PyRegion_RemoveRef(obj, start); + PyRegion_RemoveRef(obj, stop); Py_DECREF(start); Py_DECREF(stop); return NULL; @@ -152,8 +156,8 @@ PySlice_New(PyObject *start, PyObject *stop, PyObject *step) if (stop == NULL) { stop = Py_None; } - return (PyObject *)_PyBuildSlice_Consume2(Py_NewRef(start), - Py_NewRef(stop), step); + return (PyObject *)_PyBuildSlice_Consume2(PyRegion_NewRef(start), + PyRegion_NewRef(stop), step); } PyObject * @@ -348,6 +352,9 @@ slice_dealloc(PyObject *op) { PySliceObject *r = _PySlice_CAST(op); PyObject_GC_UnTrack(r); + PyRegion_RemoveRef(r, r->start); + PyRegion_RemoveRef(r, r->stop); + PyRegion_RemoveRef(r, r->step); Py_DECREF(r->step); Py_DECREF(r->start); Py_DECREF(r->stop); @@ -396,6 +403,7 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, PyObject **step_ptr) { PyObject *start=NULL, *stop=NULL, *step=NULL; + PyObject *final_start=NULL, *final_stop=NULL; PyObject *upper=NULL, *lower=NULL; int step_is_negative, cmp_result; @@ -433,12 +441,17 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, } else { lower = _PyLong_GetZero(); + PyRegion_AddRef(self, length); upper = Py_NewRef(length); } /* Compute start. */ if (self->start == Py_None) { - start = Py_NewRef(step_is_negative ? upper : lower); + final_start = step_is_negative ? upper : lower; + PyRegion_AddRef(self, final_start); + start = Py_NewRef(final_start); + /* Original */ + // start = Py_NewRef(step_is_negative ? upper : lower); } else { start = evaluate_slice_index(self->start); @@ -456,6 +469,7 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (cmp_result < 0) goto error; if (cmp_result) { + PyRegion_AddRef(self, lower); Py_SETREF(start, Py_NewRef(lower)); } } @@ -464,6 +478,7 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (cmp_result < 0) goto error; if (cmp_result) { + PyRegion_AddRef(self, upper); Py_SETREF(start, Py_NewRef(upper)); } } @@ -471,7 +486,11 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, /* Compute stop. */ if (self->stop == Py_None) { - stop = Py_NewRef(step_is_negative ? lower : upper); + final_stop = step_is_negative ? lower : upper; + PyRegion_AddRef(self, final_stop); + stop = Py_NewRef(final_stop); + /* Original */ + // stop = Py_NewRef(step_is_negative ? lower : upper); } else { stop = evaluate_slice_index(self->stop); @@ -489,6 +508,7 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (cmp_result < 0) goto error; if (cmp_result) { + PyRegion_AddRef(self, lower); Py_SETREF(stop, Py_NewRef(lower)); } } @@ -497,6 +517,7 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (cmp_result < 0) goto error; if (cmp_result) { + PyRegion_AddRef(self, upper); Py_SETREF(stop, Py_NewRef(upper)); } } @@ -505,12 +526,19 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, *start_ptr = start; *stop_ptr = stop; *step_ptr = step; + PyRegion_RemoveRef(self, upper); + PyRegion_RemoveRef(self, lower); Py_DECREF(upper); Py_DECREF(lower); return 0; error: *start_ptr = *stop_ptr = *step_ptr = NULL; + PyRegion_RemoveRef(self, upper); + PyRegion_RemoveRef(self, lower); + PyRegion_RemoveRef(self, step); + PyRegion_RemoveRef(self, start); + PyRegion_RemoveRef(self, stop); Py_XDECREF(start); Py_XDECREF(stop); Py_XDECREF(step); diff --git a/test_code/longrangeiter_1.py b/test_code/longrangeiter_1.py index bc1c717f423078..0ca4846c7ca780 100644 --- a/test_code/longrangeiter_1.py +++ b/test_code/longrangeiter_1.py @@ -9,7 +9,7 @@ r1.step = 2**50 print(f"{sys.getrefcount(r1.start)}") print(f"Initial Region: {r1}") #_lrc=3 -# input("1: Press Enter to continue...") +input("1: Press Enter to continue...") r_large = range(r1.start, r1.stop, r1.step) print(f"{r_large}") print(f"{r1.owns(r_large)}") diff --git a/test_code/range_argumentIndex.py b/test_code/range_argumentIndex.py new file mode 100644 index 00000000000000..f9f8341d32dc6c --- /dev/null +++ b/test_code/range_argumentIndex.py @@ -0,0 +1,42 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r1 = Region() +# freeze(10) +r1.start = 2**100 +r1.stop = 2**200 +r1.step = 1 +# r1.start = 1 +# r2.stop = 10 +# r3.step = 2 + +# print(f"{r1.owns(r1.start)}") + +print(f"Initial Region: {r1}") + +#print(sys.getrefcount(r.stop)) +# print(f"{r.stop}") +ra1 = range(r1.start, r1.stop, r1.step) +print(f"Region after creating range: {r1}") +print(f"{ra1}") +print(f"r1.owns(ra1): {r1.owns(ra1)}") + +r1.idx1 = 2**105 +idx2 = 2**80 +input("Continue") + +res = ra1[r1.idx1] +print(f"Region after accessing ra1[r1.idx1]: {r1}") +# r1.result_from_idx1 = res +# print(f"Region after setting result_from_idx1: {r1}") + +# res = ra1[idx2] +# print(f"Region after accessing ra1[idx2]: {r1}") # I guess PyRegion_NewRef does nothing in case that idx2 is not in the region. + +res = None +print(f"Region after deleting res: {r1}") + +# print(f"is_local(ra1[0]): {is_local(ra1[0])}") +# print(f"is_local(ra1[1]): {is_local(ra1[1])}") +# print(f"is_local(ra1[2**75]): {is_local(ra1[2**75])}") \ No newline at end of file diff --git a/test_code/range_argumentSlicing.py b/test_code/range_argumentSlicing.py new file mode 100644 index 00000000000000..618a830da8c9f4 --- /dev/null +++ b/test_code/range_argumentSlicing.py @@ -0,0 +1,38 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r1 = Region() +r1.start = 2**10000 +r1.stop = 2**20000 +r1.step = 2**100 + +print(f"Initial Region: {r1}") + +ra1 = range(r1.start, r1.stop, r1.step) +print(f"Region after creating range: {r1}") +# print(f"{ra1}") +print(f"r1.owns(ra1): {r1.owns(ra1)}") + +r1.slicing_start = 2**200 +r1.slicing_stop = 2**300 +r1.slicing_step = 2**100 +# Has a problem when r1.slicing_step is set to 2**40 +input("Continue") + +# res = ra1[r1.slicing_start:r1.slicing_stop +res = ra1[r1.slicing_start:r1.slicing_stop:r1.slicing_step] +print(f"Region after accessing ra1[:r1.slicing_stop]: {r1}") +print(f"Result of slicing: {res[2]}") +# r1.result_from_idx1 = res +# print(f"Region after setting result_from_idx1: {r1}") + +# res = ra1[idx2] +# print(f"Region after accessing ra1[idx2]: {r1}") # I guess PyRegion_NewRef does nothing in case that idx2 is not in the region. + +# res = None +# print(f"Region after deleting res: {r1}") + +# print(f"is_local(ra1[0]): {is_local(ra1[0])}") +# print(f"is_local(ra1[1]): {is_local(ra1[1])}") +# print(f"is_local(ra1[2**75]): {is_local(ra1[2**75])}") diff --git a/test_code/test1_4.py b/test_code/range_threeArgs.py similarity index 100% rename from test_code/test1_4.py rename to test_code/range_threeArgs.py diff --git a/test_code/test1_5.py b/test_code/test1_5.py deleted file mode 100644 index 6003cc7c3dfb6d..00000000000000 --- a/test_code/test1_5.py +++ /dev/null @@ -1,34 +0,0 @@ -from regions import Region, is_local -from immutable import freeze, register_freezable -import sys - -r1 = Region() -r2 = Region() -r3 = Region() -# freeze(10) -r1.start = 445435435435535340000000 -r2.stop = 4454354354355353455478980000000 -r3.step = 44543543545555 -# r1.start = 1 -# r2.stop = 10 -# r3.step = 2 -# print("sys.getrefcount(r.start): ", sys.getrefcount(r1.start)) -# print("sys.getrefcount(r.stop): ", sys.getrefcount(r2.stop)) -# print("sys.getrefcount(r.step): ", sys.getrefcount(r3.step)) -# print(f"{r1.owns(r1.start)}") -# print(f"{r2.owns(r2.stop)}") -# print(f"{r3.owns(r3.step)}") - -print(f"Initial Region: {r1}") -print(f"Initial Region: {r2}") -print(f"Initial Region: {r3}") -#print(sys.getrefcount(r.stop)) -# print(f"{r.stop}") -ra1 = range(r1.start, r2.stop, r3.step) - -print(f"{r1.owns(ra1)}") -print(f"{r2.owns(ra1)}") -print(f"{r3.owns(ra1)}") - -print(f"is_local(ra1[0]): {is_local(ra1[0])}") -print(f"is_local(ra1[1]): {is_local(ra1[1])}") \ No newline at end of file From 078c9e266098c334c3f2156438b690f4d6ec89eb Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Mon, 23 Feb 2026 21:40:10 +0100 Subject: [PATCH 06/37] finalizing range data structure migration --- Objects/abstract.c | 2 + Objects/rangeobject.c | 98 ++++++++++++++++++++++++++++++++++++------- Objects/sliceobject.c | 67 +++++++++++++++++++---------- 3 files changed, 130 insertions(+), 37 deletions(-) diff --git a/Objects/abstract.c b/Objects/abstract.c index 6f525cf2f4ff3d..234bebcb6c8a8b 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -2266,6 +2266,7 @@ _PySequence_IterSearch(PyObject *seq, PyObject *obj, int operation) } cmp = PyObject_RichCompareBool(item, obj, Py_EQ); + PyRegion_RemoveLocalRef(item); Py_DECREF(item); if (cmp < 0) goto Fail; @@ -2314,6 +2315,7 @@ _PySequence_IterSearch(PyObject *seq, PyObject *obj, int operation) n = -1; /* fall through */ Done: + PyRegion_RemoveLocalRef(it); Py_DECREF(it); return n; diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 0e3060802647c7..84bbcdd04a8043 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -366,6 +366,7 @@ compute_item(rangeobject *r, PyObject *i) return NULL; } result = PyNumber_Add(r->start, incr); + assert(PyRegion_IsLocal(incr)); // incr should be local, so no write barrier here. Py_DECREF(incr); } return result; @@ -395,6 +396,9 @@ compute_range_item(rangeobject *r, PyObject *arg) return NULL; } } else { + if(PyRegion_AddLocalRef(arg)) { + return NULL; + } i = Py_NewRef(arg); } @@ -408,10 +412,12 @@ compute_range_item(rangeobject *r, PyObject *arg) cmp_result = PyObject_RichCompareBool(i, r->length, Py_GE); } if (cmp_result == -1) { - Py_DECREF(i); + PyRegion_RemoveLocalRef(i); + Py_DECREF(i); return NULL; } if (cmp_result == 1) { + PyRegion_RemoveLocalRef(i); Py_DECREF(i); PyErr_SetString(PyExc_IndexError, "range object index out of range"); @@ -419,6 +425,7 @@ compute_range_item(rangeobject *r, PyObject *arg) } result = compute_item(r, i); + PyRegion_RemoveLocalRef(i); Py_DECREF(i); return result; } @@ -454,17 +461,17 @@ compute_slice(rangeobject *r, PyObject *_slice) substep = PyNumber_Multiply(r->step, step); if (substep == NULL) goto fail; - PyRegion_RemoveRef(slice, step); + PyRegion_RemoveLocalRef(step); Py_CLEAR(step); substart = compute_item(r, start); if (substart == NULL) goto fail; - PyRegion_RemoveRef(slice, start); + PyRegion_RemoveLocalRef(start); Py_CLEAR(start); substop = compute_item(r, stop); if (substop == NULL) goto fail; - PyRegion_RemoveRef(slice, stop); + PyRegion_RemoveLocalRef(stop); Py_CLEAR(stop); result = make_range_object(Py_TYPE(r), substart, substop, substep); @@ -472,9 +479,12 @@ compute_slice(rangeobject *r, PyObject *_slice) return (PyObject *) result; } fail: - PyRegion_RemoveRef(slice, start); - PyRegion_RemoveRef(slice, stop); - PyRegion_RemoveRef(slice, step); + PyRegion_RemoveLocalRef(start); + PyRegion_RemoveLocalRef(stop); + PyRegion_RemoveLocalRef(step); + PyRegion_RemoveLocalRef(substart); + PyRegion_RemoveLocalRef(substop); + PyRegion_RemoveLocalRef(substep); Py_XDECREF(start); Py_XDECREF(stop); Py_XDECREF(step); @@ -525,6 +535,8 @@ range_contains_long(rangeobject *r, PyObject *ob) /* result = ((int(ob) - start) % step) == 0 */ result = PyObject_RichCompareBool(tmp2, zero, Py_EQ); end: + assert(PyRegion_IsLocal(tmp1) || Py_IsImmutable(tmp1)); // tmp1 should be local or immutable, so no write barrier here. + assert(PyRegion_IsLocal(tmp2) || Py_IsImmutable(tmp2)); // tmp2 should be local or immutable, so no write barrier here. Py_XDECREF(tmp1); Py_XDECREF(tmp2); return result; @@ -631,28 +643,50 @@ range_hash(PyObject *op) t = PyTuple_New(3); if (!t) return -1; - PyTuple_SET_ITEM(t, 0, Py_NewRef(r->length)); + + // First element of the tuple is always the length of the range + PyObject* length = PyRegion_NewRef(r->length); + if(length == NULL) return -1; + PyTuple_SET_ITEM(t, 0, length); + + // len == 0 or not cmp_result = PyObject_Not(r->length); if (cmp_result == -1) + { goto end; + } + + // len is 0 if (cmp_result == 1) { PyTuple_SET_ITEM(t, 1, Py_NewRef(Py_None)); PyTuple_SET_ITEM(t, 2, Py_NewRef(Py_None)); } else { - PyTuple_SET_ITEM(t, 1, Py_NewRef(r->start)); + // Second element of the tuple is always the start of the range + PyObject* start = PyRegion_NewRef(r->start); + if(start == NULL) return -1; + PyTuple_SET_ITEM(t, 1, start); + + // Check if len == 1 or not cmp_result = PyObject_RichCompareBool(r->length, _PyLong_GetOne(), Py_EQ); if (cmp_result == -1) + { goto end; + } + // len is 1, step doesn't matter if (cmp_result == 1) { PyTuple_SET_ITEM(t, 2, Py_NewRef(Py_None)); } else { - PyTuple_SET_ITEM(t, 2, Py_NewRef(r->step)); + // The third element of the tuple is the step of the range when len > 1 + PyObject* step = PyRegion_NewRef(r->step); + if(step == NULL) return -1; + PyTuple_SET_ITEM(t, 2, step); } } result = PyObject_Hash(t); end: + PyRegion_RemoveLocalRef(t); Py_DECREF(t); return result; } @@ -705,6 +739,7 @@ range_index(PyObject *self, PyObject *ob) /* idx = (ob - r.start) // r.step */ PyObject *sidx = PyNumber_FloorDivide(idx, r->step); + assert(PyRegion_IsLocal(idx) || Py_IsImmutable(idx)); // idx should be local, so no write barrier here. Py_DECREF(idx); return sidx; } @@ -715,7 +750,6 @@ range_index(PyObject *self, PyObject *ob) } static PySequenceMethods range_as_sequence = { - // No need for barrier for all of these since there is no modification to "rangeobject" range_length, /* sq_length */ 0, /* sq_concat */ 0, /* sq_repeat */ @@ -768,11 +802,11 @@ range_subscript(PyObject *op, PyObject *item) if (!i) return NULL; result = compute_range_item(self, i); + PyRegion_RemoveLocalRef(i); Py_DECREF(i); return result; } if (PySlice_Check(item)) { - // TODO return compute_slice(self, item); } PyErr_Format(PyExc_TypeError, @@ -856,7 +890,6 @@ PyTypeObject PyRange_Type = { &range_as_number, /* tp_as_number */ &range_as_sequence, /* tp_as_sequence */ &range_as_mapping, /* tp_as_mapping */ - // TODO range_hash, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ @@ -940,6 +973,9 @@ rangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) return Py_BuildValue("N(N)O", _PyEval_GetBuiltin(&_Py_ID(iter)), range, Py_None); err: + assert(PyRegion_IsLocal(start) || Py_IsImmutable(start)); // start should be local or immutable, so no write barrier here. + assert(PyRegion_IsLocal(stop) || Py_IsImmutable(stop)); + assert(PyRegion_IsLocal(step) || Py_IsImmutable(step)); Py_XDECREF(start); Py_XDECREF(stop); Py_XDECREF(step); @@ -1079,6 +1115,11 @@ static PyObject * longrangeiter_len(PyObject *op, PyObject *Py_UNUSED(ignored)) { longrangeiterobject *r = (longrangeiterobject*)op; + // 0 on success + if(PyRegion_AddLocalRef(r->len)) { + return NULL; + } + // We dont do any assignment here, so No AddRef Py_INCREF(r->len); return r->len; } @@ -1098,10 +1139,17 @@ longrangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) Py_DECREF(product); if (stop == NULL) return NULL; + if(PyRegion_AddLocalRefs(r->start, r->step)) { + Py_DECREF(stop); + return NULL; + } range = (PyObject*)make_range_object(&PyRange_Type, Py_NewRef(r->start), stop, Py_NewRef(r->step)); if (range == NULL) { + PyRegion_RemoveLocalRef(r->start); // Because r is has a relationship to r->start, so No RemoveRef here + PyRegion_RemoveLocalRef(r->step); Py_DECREF(r->start); + assert(PyRegion_IsLocal(stop) || Py_IsImmutable(stop)); Py_DECREF(stop); Py_DECREF(r->step); return NULL; @@ -1137,6 +1185,7 @@ longrangeiter_setstate(PyObject *op, PyObject *state) if (product == NULL) return NULL; PyObject *new_start = PyNumber_Add(r->start, product); + assert(PyRegion_IsLocal(product) || Py_IsImmutable(product)); Py_DECREF(product); if (new_start == NULL) return NULL; @@ -1146,8 +1195,19 @@ longrangeiter_setstate(PyObject *op, PyObject *state) return NULL; } PyObject *tmp = r->start; + if(PyRegion_TakeRef(r, new_start)) { + Py_DECREF(new_start); + Py_DECREF(new_len); + return NULL; + } r->start = new_start; - Py_SETREF(r->len, new_len); + if(PyRegion_XSETREF(r, r->len, new_len)) { + Py_DECREF(new_start); + Py_DECREF(new_len); + return NULL; + } + // Original: Py_SETREF(r->len, new_len); + PyRegion_RemoveRef(r, tmp); Py_DECREF(tmp); Py_RETURN_NONE; } @@ -1185,12 +1245,19 @@ longrangeiter_next(PyObject *op) } PyObject *new_len = PyNumber_Subtract(r->len, _PyLong_GetOne()); if (new_len == NULL) { + assert(PyRegion_IsLocal(new_start) || Py_IsImmutable(new_start)); Py_DECREF(new_start); return NULL; } PyObject *result = r->start; r->start = new_start; - Py_SETREF(r->len, new_len); + // r->len is not Local, so we need to use PyRegion_XSETREF to update it. + if(PyRegion_XSETREF(r, r->len, new_len)) { + Py_DECREF(new_start); + Py_DECREF(new_len); + return NULL; + } + // Original: Py_SETREF(r->len, new_len); return result; } @@ -1403,6 +1470,7 @@ range_reverse(PyObject *seq, PyObject *Py_UNUSED(ignored)) return (PyObject *)it; create_failure: + PyRegion_RemoveLocalRef(it); Py_DECREF(it); return NULL; } diff --git a/Objects/sliceobject.c b/Objects/sliceobject.c index 25f99ef5acf1b8..5381c4cc77f40e 100644 --- a/Objects/sliceobject.c +++ b/Objects/sliceobject.c @@ -395,7 +395,10 @@ evaluate_slice_index(PyObject *v) /* Compute slice indices given a slice and length. Return -1 on failure. Used by slice.indices and rangeobject slicing. Assumes that `len` is a - nonnegative instance of PyLong. */ + nonnegative instance of PyLong. + + For now, start_ptr, stop_ptr, step_ptr must be local. +*/ int _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, @@ -441,14 +444,18 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, } else { lower = _PyLong_GetZero(); - PyRegion_AddRef(self, length); + if(PyRegion_AddLocalRef(length)) { + goto error; + } upper = Py_NewRef(length); } /* Compute start. */ if (self->start == Py_None) { final_start = step_is_negative ? upper : lower; - PyRegion_AddRef(self, final_start); + if(PyRegion_AddLocalRef(final_start)) { + goto error; + } start = Py_NewRef(final_start); /* Original */ // start = Py_NewRef(step_is_negative ? upper : lower); @@ -461,7 +468,10 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (_PyLong_IsNegative((PyLongObject *)start)) { /* start += length */ PyObject *tmp = PyNumber_Add(start, length); - Py_SETREF(start, tmp); + if(PyRegion_XSETLOCALREF(start, tmp)) { + goto error; + } + // Original: Py_SETREF(start, tmp); if (start == NULL) goto error; @@ -469,8 +479,10 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (cmp_result < 0) goto error; if (cmp_result) { - PyRegion_AddRef(self, lower); - Py_SETREF(start, Py_NewRef(lower)); + if(PyRegion_XSETLOCALNEWREF(start, lower)) { + goto error; + } + // Original: Py_SETREF(start, Py_NewRef(lower)); } } else { @@ -478,8 +490,10 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (cmp_result < 0) goto error; if (cmp_result) { - PyRegion_AddRef(self, upper); - Py_SETREF(start, Py_NewRef(upper)); + if(PyRegion_XSETLOCALNEWREF(start, upper)) { + goto error; + } + // Original: Py_SETREF(start, Py_NewRef(upper)); } } } @@ -487,7 +501,9 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, /* Compute stop. */ if (self->stop == Py_None) { final_stop = step_is_negative ? lower : upper; - PyRegion_AddRef(self, final_stop); + if(PyRegion_AddLocalRef(final_stop)) { + goto error; + } stop = Py_NewRef(final_stop); /* Original */ // stop = Py_NewRef(step_is_negative ? lower : upper); @@ -500,7 +516,10 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (_PyLong_IsNegative((PyLongObject *)stop)) { /* stop += length */ PyObject *tmp = PyNumber_Add(stop, length); - Py_SETREF(stop, tmp); + if(PyRegion_XSETLOCALREF(stop, tmp)) { + goto error; + } + // Original: Py_SETREF(stop, tmp); if (stop == NULL) goto error; @@ -508,8 +527,10 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (cmp_result < 0) goto error; if (cmp_result) { - PyRegion_AddRef(self, lower); - Py_SETREF(stop, Py_NewRef(lower)); + if(PyRegion_XSETLOCALNEWREF(stop, lower)) { + goto error; + } + // Original: Py_SETREF(stop, Py_NewRef(lower)); } } else { @@ -517,28 +538,30 @@ _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, if (cmp_result < 0) goto error; if (cmp_result) { - PyRegion_AddRef(self, upper); - Py_SETREF(stop, Py_NewRef(upper)); + if(PyRegion_XSETLOCALNEWREF(stop, upper)) { + goto error; + } + // Original: Py_SETREF(stop, Py_NewRef(upper)); } } } - + // Call TakeRef if the signature is changed. *start_ptr = start; *stop_ptr = stop; *step_ptr = step; - PyRegion_RemoveRef(self, upper); - PyRegion_RemoveRef(self, lower); + PyRegion_RemoveLocalRef(upper); + PyRegion_RemoveLocalRef(lower); Py_DECREF(upper); Py_DECREF(lower); return 0; error: *start_ptr = *stop_ptr = *step_ptr = NULL; - PyRegion_RemoveRef(self, upper); - PyRegion_RemoveRef(self, lower); - PyRegion_RemoveRef(self, step); - PyRegion_RemoveRef(self, start); - PyRegion_RemoveRef(self, stop); + PyRegion_RemoveLocalRef(upper); + PyRegion_RemoveLocalRef(lower); + PyRegion_RemoveLocalRef(step); + PyRegion_RemoveLocalRef(start); + PyRegion_RemoveLocalRef(stop); Py_XDECREF(start); Py_XDECREF(stop); Py_XDECREF(step); From 21781b96c01af7a9257ca5c852d8f861d03fb993 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Tue, 24 Feb 2026 10:21:22 +0100 Subject: [PATCH 07/37] add test cases for rnage data structure --- test_code/{ => range_test}/longrangeiter_1.py | 0 test_code/{ => range_test}/longrangeiter_2.py | 0 .../{ => range_test}/range_argumentIndex.py | 0 .../{ => range_test}/range_argumentSlicing.py | 8 +++---- test_code/range_test/range_testHash.py | 22 +++++++++++++++++++ test_code/{ => range_test}/range_threeArgs.py | 0 .../rangeiter_vs_longrangeiter.py | 0 7 files changed, 26 insertions(+), 4 deletions(-) rename test_code/{ => range_test}/longrangeiter_1.py (100%) rename test_code/{ => range_test}/longrangeiter_2.py (100%) rename test_code/{ => range_test}/range_argumentIndex.py (100%) rename test_code/{ => range_test}/range_argumentSlicing.py (72%) create mode 100644 test_code/range_test/range_testHash.py rename test_code/{ => range_test}/range_threeArgs.py (100%) rename test_code/{ => range_test}/rangeiter_vs_longrangeiter.py (100%) diff --git a/test_code/longrangeiter_1.py b/test_code/range_test/longrangeiter_1.py similarity index 100% rename from test_code/longrangeiter_1.py rename to test_code/range_test/longrangeiter_1.py diff --git a/test_code/longrangeiter_2.py b/test_code/range_test/longrangeiter_2.py similarity index 100% rename from test_code/longrangeiter_2.py rename to test_code/range_test/longrangeiter_2.py diff --git a/test_code/range_argumentIndex.py b/test_code/range_test/range_argumentIndex.py similarity index 100% rename from test_code/range_argumentIndex.py rename to test_code/range_test/range_argumentIndex.py diff --git a/test_code/range_argumentSlicing.py b/test_code/range_test/range_argumentSlicing.py similarity index 72% rename from test_code/range_argumentSlicing.py rename to test_code/range_test/range_argumentSlicing.py index 618a830da8c9f4..4e067e026c0222 100644 --- a/test_code/range_argumentSlicing.py +++ b/test_code/range_test/range_argumentSlicing.py @@ -22,13 +22,13 @@ # res = ra1[r1.slicing_start:r1.slicing_stop res = ra1[r1.slicing_start:r1.slicing_stop:r1.slicing_step] -print(f"Region after accessing ra1[:r1.slicing_stop]: {r1}") -print(f"Result of slicing: {res[2]}") +print(f"Region after accessing ra1[:r1.slicing_stop]: {r1}") # LRC should not change since the slice object is destroyed. +# print(f"Result of slicing: {res[2]}") # r1.result_from_idx1 = res # print(f"Region after setting result_from_idx1: {r1}") -# res = ra1[idx2] -# print(f"Region after accessing ra1[idx2]: {r1}") # I guess PyRegion_NewRef does nothing in case that idx2 is not in the region. +test_slic_obj = slice(r1.slicing_start, r1.slicing_stop, r1.slicing_step) +print(f"Region after creating test_slic_obj: {r1}") # LRC +3 since the slice object is still alive. # res = None # print(f"Region after deleting res: {r1}") diff --git a/test_code/range_test/range_testHash.py b/test_code/range_test/range_testHash.py new file mode 100644 index 00000000000000..7d71fbf7492cdf --- /dev/null +++ b/test_code/range_test/range_testHash.py @@ -0,0 +1,22 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +import sys + +r1 = Region() +r1.start = 2**1000 +r1.stop = 2**2000 +r1.step = 2**100 + +print(f"Initial Region: {r1}") + +ra1 = range(r1.start, r1.stop, r1.step) +print(f"Region after creating range: {r1}") +# print(f"{ra1}") +print(f"r1.owns(ra1): {r1.owns(ra1)}") + +input("Continue") +h = hash(ra1) +print(f"Region after hashing ra1: {r1}") +print(f"Hash of ra1: {h}") +print(f"{r1.owns(ra1)}") +# print(f"{ra1}") \ No newline at end of file diff --git a/test_code/range_threeArgs.py b/test_code/range_test/range_threeArgs.py similarity index 100% rename from test_code/range_threeArgs.py rename to test_code/range_test/range_threeArgs.py diff --git a/test_code/rangeiter_vs_longrangeiter.py b/test_code/range_test/rangeiter_vs_longrangeiter.py similarity index 100% rename from test_code/rangeiter_vs_longrangeiter.py rename to test_code/range_test/rangeiter_vs_longrangeiter.py From 47bf1f3170fafb9abc241de0d85381c129d664f6 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Tue, 24 Feb 2026 10:25:45 +0100 Subject: [PATCH 08/37] fix assert _Py_IsImmutable bug --- Objects/rangeobject.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 84bbcdd04a8043..6887ddfc239cb2 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -535,8 +535,8 @@ range_contains_long(rangeobject *r, PyObject *ob) /* result = ((int(ob) - start) % step) == 0 */ result = PyObject_RichCompareBool(tmp2, zero, Py_EQ); end: - assert(PyRegion_IsLocal(tmp1) || Py_IsImmutable(tmp1)); // tmp1 should be local or immutable, so no write barrier here. - assert(PyRegion_IsLocal(tmp2) || Py_IsImmutable(tmp2)); // tmp2 should be local or immutable, so no write barrier here. + assert(PyRegion_IsLocal(tmp1) || _Py_IsImmutable(tmp1)); // tmp1 should be local or immutable, so no write barrier here. + assert(PyRegion_IsLocal(tmp2) || _Py_IsImmutable(tmp2)); // tmp2 should be local or immutable, so no write barrier here. Py_XDECREF(tmp1); Py_XDECREF(tmp2); return result; @@ -739,7 +739,7 @@ range_index(PyObject *self, PyObject *ob) /* idx = (ob - r.start) // r.step */ PyObject *sidx = PyNumber_FloorDivide(idx, r->step); - assert(PyRegion_IsLocal(idx) || Py_IsImmutable(idx)); // idx should be local, so no write barrier here. + assert(PyRegion_IsLocal(idx) || _Py_IsImmutable(idx)); // idx should be local, so no write barrier here. Py_DECREF(idx); return sidx; } @@ -973,9 +973,9 @@ rangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) return Py_BuildValue("N(N)O", _PyEval_GetBuiltin(&_Py_ID(iter)), range, Py_None); err: - assert(PyRegion_IsLocal(start) || Py_IsImmutable(start)); // start should be local or immutable, so no write barrier here. - assert(PyRegion_IsLocal(stop) || Py_IsImmutable(stop)); - assert(PyRegion_IsLocal(step) || Py_IsImmutable(step)); + assert(PyRegion_IsLocal(start) || _Py_IsImmutable(start)); // start should be local or immutable, so no write barrier here. + assert(PyRegion_IsLocal(stop) || _Py_IsImmutable(stop)); + assert(PyRegion_IsLocal(step) || _Py_IsImmutable(step)); Py_XDECREF(start); Py_XDECREF(stop); Py_XDECREF(step); @@ -1149,7 +1149,7 @@ longrangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) PyRegion_RemoveLocalRef(r->start); // Because r is has a relationship to r->start, so No RemoveRef here PyRegion_RemoveLocalRef(r->step); Py_DECREF(r->start); - assert(PyRegion_IsLocal(stop) || Py_IsImmutable(stop)); + assert(PyRegion_IsLocal(stop) || _Py_IsImmutable(stop)); Py_DECREF(stop); Py_DECREF(r->step); return NULL; @@ -1185,7 +1185,7 @@ longrangeiter_setstate(PyObject *op, PyObject *state) if (product == NULL) return NULL; PyObject *new_start = PyNumber_Add(r->start, product); - assert(PyRegion_IsLocal(product) || Py_IsImmutable(product)); + assert(PyRegion_IsLocal(product) || _Py_IsImmutable(product)); Py_DECREF(product); if (new_start == NULL) return NULL; @@ -1245,7 +1245,7 @@ longrangeiter_next(PyObject *op) } PyObject *new_len = PyNumber_Subtract(r->len, _PyLong_GetOne()); if (new_len == NULL) { - assert(PyRegion_IsLocal(new_start) || Py_IsImmutable(new_start)); + assert(PyRegion_IsLocal(new_start) || _Py_IsImmutable(new_start)); Py_DECREF(new_start); return NULL; } From 44c8ddb19051973ca648b0bff30e8d851fc47c60 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Mon, 2 Mar 2026 11:35:07 +0100 Subject: [PATCH 09/37] add and update testcases --- test_code/range_test/range_test.py | 3 + test_code/report_code/cross_region.py | 11 ++++ test_code/report_code/freeze_item.py | 1 + .../report_code/move_from_localR_to_R.py | 6 +- test_code/report_code/share_nothing.py | 22 ++++++++ test_code/set_test/setiter_test1.py | 56 +++++++++++++++++++ test_code/set_test/test1.py | 44 +++++++++++++++ test_code/set_test/test2.py | 38 +++++++++++++ test_code/set_test/test3.py | 23 ++++++++ 9 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 test_code/range_test/range_test.py create mode 100644 test_code/report_code/cross_region.py create mode 100644 test_code/report_code/share_nothing.py create mode 100644 test_code/set_test/setiter_test1.py create mode 100644 test_code/set_test/test1.py create mode 100644 test_code/set_test/test2.py create mode 100644 test_code/set_test/test3.py diff --git a/test_code/range_test/range_test.py b/test_code/range_test/range_test.py new file mode 100644 index 00000000000000..cb0d908013f861 --- /dev/null +++ b/test_code/range_test/range_test.py @@ -0,0 +1,3 @@ +ra = range(1,100,2) +input("Continue") +ra_iter = iter(ra) \ No newline at end of file diff --git a/test_code/report_code/cross_region.py b/test_code/report_code/cross_region.py new file mode 100644 index 00000000000000..83f64a36aa536a --- /dev/null +++ b/test_code/report_code/cross_region.py @@ -0,0 +1,11 @@ +from regions import Region, is_local + +r1 = Region() +r2 = Region() +r3 = Region() +r2.obj = {"key": "value"} + +r1.a = r2 +# r1.a = r2.obj # RuntimeError: References to objects in other regions are forbidden +# r1.b = r2 # RuntimeError: Regions are not allowed to have multiple parents +# r3.c = r2 # RuntimeError: Regions are not allowed to have multiple parents \ No newline at end of file diff --git a/test_code/report_code/freeze_item.py b/test_code/report_code/freeze_item.py index dc2aa4b863fefc..8c771d0b6df2f1 100644 --- a/test_code/report_code/freeze_item.py +++ b/test_code/report_code/freeze_item.py @@ -2,6 +2,7 @@ from immutable import freeze, register_freezable r = Region() +class A: pass obj = {"key": "value"} print(f"Initial Region: {r}") r.start = obj diff --git a/test_code/report_code/move_from_localR_to_R.py b/test_code/report_code/move_from_localR_to_R.py index 4c03706d6b95a0..da420c8f7cfe9f 100644 --- a/test_code/report_code/move_from_localR_to_R.py +++ b/test_code/report_code/move_from_localR_to_R.py @@ -11,4 +11,8 @@ print(f"Obj is in the local region: {is_local(obj)}") # False print(f"Child obj is in the local region: {is_local(child_obj)}") # False print(f"Region owns obj: {r.owns(obj)}") # True -print(f"Region owns child obj: {r.owns(child_obj)}") # True \ No newline at end of file +print(f"Region owns child obj: {r.owns(child_obj)}") # True + +obj["key"] = "new_value" +print(f"obj after modification: {obj}") +print(f"Region after modifying obj: {r}") \ No newline at end of file diff --git a/test_code/report_code/share_nothing.py b/test_code/report_code/share_nothing.py new file mode 100644 index 00000000000000..18b787aac49101 --- /dev/null +++ b/test_code/report_code/share_nothing.py @@ -0,0 +1,22 @@ +from concurrent import interpreters + +# Create a subinterpreter +interp = interpreters.create() + +my_list = [1, 2, 3] + +# Subinterpreter has NO access to main's objects +interp.exec(""" +try: + print(my_list) +except NameError as e: + print(f"NameError: {e}") +""") + +# Share by COPYING the data +interp.exec(f""" +copied_list = {my_list} # A copy, not a reference +print(f"Received a copy: {{copied_list}}") +""") + +interp.close() \ No newline at end of file diff --git a/test_code/set_test/setiter_test1.py b/test_code/set_test/setiter_test1.py new file mode 100644 index 00000000000000..fb1be61696acfc --- /dev/null +++ b/test_code/set_test/setiter_test1.py @@ -0,0 +1,56 @@ +# Test Case for testing setiterobject's dealloc and traverse functionsfrom regions import Region, is_local +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.word=A() +r.word2=A() +r.word3=A() +r.word4=A() +r.word5=A() + +r.arr = [r.word, r.word2, r.word3, r.word4, r.word5] +print(f"Region after setting word: {r}") +s = set(r.arr) +print(f"Region after creating set: {r}") + +it = iter(s) +print(f"Region after creating set iterator: {r}") +# input("Press Enter to print set iterator...") +# r.it = it +# input("Press Enter to print set iterator...") +# print(f"Region after setting iterator: {r}") # iterator that "it" points to is moved into the region, so "s" as well. Since "s" is moved into the region, no borrowed references anymore from outside of the region. LRC is reset to the initial value. Then +2 from "it" and "s" variable itself. +# s=None +# it=None +# print(f"Region after setting set and iterator to None: {r}") + +a1 = next(it) +print(f"Region after calling next on set iterator: {r}") +a2 = next(it) +print(f"Region after calling next on set iterator again: {r}") +r.a3 = next(it) +print(f"Region after calling next on set iterator again: {r}") +a4 = next(it) +print(f"Region after calling next on set iterator again: {r}") +it = None +print(f"Region after setting iterator to None: {r}") + +# r.it = iter(s) +# print(f"Region after creating set iterator: {r}") +# print(f"{r.it}") +# print(f"Region after printing set iterator: {r}") +# a1 = next(r.it) +# print(f"Region after calling next on set iterator: {r}") +# a2 = next(r.it) +# print(f"Region after calling next on set iterator again: {r}") +# r.a3 = next(r.it) +# print(f"Region after calling next on set iterator again: {r}") +# a4 = next(r.it) +# print(f"Region after calling next on set iterator again: {r}") +# r.it = None +# print(f"Region after setting iterator to None: {r}") diff --git a/test_code/set_test/test1.py b/test_code/set_test/test1.py new file mode 100644 index 00000000000000..561ac5aa63f236 --- /dev/null +++ b/test_code/set_test/test1.py @@ -0,0 +1,44 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +print(f"Initial Region: {r}") +# r.word=2**1000 +# r.word2=2**2000 +# r.word3=2**3000 +class A: pass; +freeze(A()) + +r.word=A() +r.word2=A() + + +r.arr = [r.word, r.word2] +print(f"Region after setting word: {r}") +# input("Press Enter to create set...") +s = set(r.arr) +print(f"Region after creating set: {r}") +print(f"{s}") +print(f"Region after printing set: {r}") + +print("----------------------------------------------------------") +r2 = Region() +r2.word=A() +r2.word2=A() +word3=A() +word4=A() +print(f"New Region: {r2}") +r2.arr = [r2.word, r2.word2, word3, word4] # obj in word3 and word4 are added to the region r2, and word3 and word4 points to its object. +print(f"Region after setting word in new region: {r2}") +s2 = set(r2.arr) # Now, the set points to all objs in the region r2 +print(f"Region after creating set in new region: {r2}") + +# ------ Uncomment this makes an error to LRC count --------------- +# for i in s: + # print(hex(id(i))) + +# print(hex(id(r.word)), hex(id(r.word2)), hex(id(r.word3))) +# ----------------------------------------------------------------- + +s = None +print(f"Region after setting set to None: {r}") \ No newline at end of file diff --git a/test_code/set_test/test2.py b/test_code/set_test/test2.py new file mode 100644 index 00000000000000..c0080a324245f9 --- /dev/null +++ b/test_code/set_test/test2.py @@ -0,0 +1,38 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.word=A() +r.word2=A() +r.word3=A() +r.word4=A() +r.arr = [r.word, r.word2, r.word3, r.word4] +print(f"Region after setting word: {r}") # LRC=2 +s = set(r.arr) +print(f"Region after creating set: {r}") # LRC=6 since it borrows word, word2, word3, and word4. "s" does not matter here since set is not in the region yet. +print(f"{s}") +print(f"Region after printing set: {r}") # LRC=6 +r.set = s +print(f"Region after moving set into the region: {r}") # LRC=3: -4 from subtracting word, word2, word3, and word4 to the region since the set obj is moved into the region, +1 from s +input("Press Enter to create set...") +s2 = set(r.set) +print(f"Region after creating set from set in the region: {r}") +# print(f"{s2}") +# print(f"Region after printing set2: {r}") + +# print(f"{s}") + +# ------ Uncomment this makes an error to LRC count --------------- +# for i in s: +# print(hex(id(i))) +# print(f"Region after iterating through set: {r}") +# print(hex(id(r.word)), hex(id(r.word2)), hex(id(r.word3))) +# ----------------------------------------------------------------- + +s2 = None +print(f"Region after setting set to None: {r}") \ No newline at end of file diff --git a/test_code/set_test/test3.py b/test_code/set_test/test3.py new file mode 100644 index 00000000000000..36b9d26847533b --- /dev/null +++ b/test_code/set_test/test3.py @@ -0,0 +1,23 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable, isfrozen + +r = Region() +print(f"Initial Region: {r}") + +class A: pass +freeze(A()) +r.word={A(): "value", A(): "value2"} +print(f"Region after setting word: {r}") +s = set(r.word) +print(f"Region after creating set: {r}") +print(f"{s}") + +print("\n") +r2 = Region() +print(f"New Region: {r2}") +r2.word = {"key": "value", "key2": "value2"} +print(f"isfrozen(r2.word): {isfrozen(r2.word["key"])}") +print(f"New Region after setting word: {r2}") +s2 = set(r2.word) +print(f"New Region after creating set: {r2}") +print(f"{s2}") \ No newline at end of file From cdccb3fa4eb21f77a718105027070fa0f470535a Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Mon, 2 Mar 2026 11:38:53 +0100 Subject: [PATCH 10/37] update set migration on new, dealloc, repr, and setiter (not finished) --- Objects/setobject.c | 93 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index faf3bd34da8588..885db906bf6461 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -45,6 +45,7 @@ #include "stringlib/eq.h" // unicode_eq() #include // offsetof() #include "clinic/setobject.c.h" +#include "region.h" /*[clinic input] class set "PySetObject *" "&PySet_Type" @@ -166,8 +167,12 @@ set_add_entry_takeref(PySetObject *so, PyObject *key, Py_hash_t hash) && unicode_eq(startkey, key)) goto found_active; table = so->table; + if(PyRegion_AddLocalRef(startkey)) { + return -1; + } Py_INCREF(startkey); cmp = PyObject_RichCompareBool(startkey, key, Py_EQ); + PyRegion_RemoveLocalRef(startkey); Py_DECREF(startkey); if (cmp > 0) goto found_active; @@ -191,6 +196,7 @@ set_add_entry_takeref(PySetObject *so, PyObject *key, Py_hash_t hash) if (freeslot == NULL) goto found_unused; FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used + 1); + if(PyRegion_TakeRef(so, key)) return -1; freeslot->key = key; freeslot->hash = hash; return 0; @@ -198,6 +204,7 @@ set_add_entry_takeref(PySetObject *so, PyObject *key, Py_hash_t hash) found_unused: so->fill++; FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used + 1); + if(PyRegion_TakeRef(so, key)) return -1; entry->key = key; entry->hash = hash; if ((size_t)so->fill*5 < mask*3) @@ -205,10 +212,12 @@ set_add_entry_takeref(PySetObject *so, PyObject *key, Py_hash_t hash) return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4); found_active: + PyRegion_RemoveLocalRef(key); Py_DECREF(key); return 0; comparison_error: + PyRegion_RemoveLocalRef(key); Py_DECREF(key); return -1; } @@ -217,7 +226,9 @@ static int set_add_entry(PySetObject *so, PyObject *key, Py_hash_t hash) { _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(so); - + if (PyRegion_AddLocalRef(key)) { + return -1; + } return set_add_entry_takeref(so, Py_NewRef(key), hash); } @@ -234,6 +245,7 @@ set_unhashable_type(PyObject *key) PyErr_Format(PyExc_TypeError, "cannot use '%T' as a set element (%S)", key, exc); + assert(PyRegion_IsLocal(exc)); Py_DECREF(exc); } @@ -259,8 +271,8 @@ a callback in the middle of a set_table_resize(), see issue 1456209. The caller is responsible for updating the key's reference count and the setobject's fill and used fields. */ -static void -set_insert_clean(setentry *table, size_t mask, PyObject *key, Py_hash_t hash) +static int +set_insert_clean(PySetObject* set, setentry *table, size_t mask, PyObject *key, Py_hash_t hash) { setentry *entry; size_t perturb = hash; @@ -282,8 +294,12 @@ set_insert_clean(setentry *table, size_t mask, PyObject *key, Py_hash_t hash) i = (i * 5 + 1 + perturb) & mask; } found_null: + if(PyRegion_TakeRef(set, key)) { + return -1; + } entry->key = key; entry->hash = hash; + return 0; } /* ======== End logic for probing the hash table ========================== */ @@ -356,14 +372,17 @@ set_table_resize(PySetObject *so, Py_ssize_t minused) if (so->fill == so->used) { for (entry = oldtable; entry <= oldtable + oldmask; entry++) { if (entry->key != NULL) { - set_insert_clean(newtable, newmask, entry->key, entry->hash); + // should never fail since it already has an element. + int res = set_insert_clean(so, newtable, newmask, entry->key, entry->hash); + assert(res==0); } } } else { so->fill = so->used; for (entry = oldtable; entry <= oldtable + oldmask; entry++) { if (entry->key != NULL && entry->key != dummy) { - set_insert_clean(newtable, newmask, entry->key, entry->hash); + int res = set_insert_clean(so, newtable, newmask, entry->key, entry->hash); + assert(res==0); } } } @@ -557,6 +576,7 @@ set_dealloc(PyObject *self) for (entry = so->table; used > 0; entry++) { if (entry->key && entry->key != dummy) { used--; + PyRegion_RemoveRef(so, entry->key); Py_DECREF(entry->key); } } @@ -593,15 +613,22 @@ set_repr_lock_held(PySetObject *so) Py_ssize_t pos = 0, idx = 0; setentry *entry; while (set_next(so, &pos, &entry)) { - PyList_SET_ITEM(keys, idx++, Py_NewRef(entry->key)); + PyObject *entry_key = PyRegion_NewRef(entry->key); + if(entry_key == NULL) { + Py_DECREF(keys); + goto done; + } + PyList_SET_ITEM(keys, idx++, entry_key); } /* repr(keys)[1:-1] */ listrepr = PyObject_Repr(keys); + assert(PyRegion_IsLocal(keys)); Py_DECREF(keys); if (listrepr == NULL) goto done; tmp = PyUnicode_Substring(listrepr, 1, PyUnicode_GET_LENGTH(listrepr)-1); + assert(PyRegion_IsLocal(listrepr)); Py_DECREF(listrepr); if (tmp == NULL) goto done; @@ -613,6 +640,7 @@ set_repr_lock_held(PySetObject *so) listrepr); else result = PyUnicode_FromFormat("{%U}", listrepr); + assert(PyRegion_IsLocal(listrepr)); Py_DECREF(listrepr); done: Py_ReprLeave((PyObject*)so); @@ -672,6 +700,12 @@ set_merge_lock_held(PySetObject *so, PyObject *otherset) key = other_entry->key; if (key != NULL) { assert(so_entry->key == NULL); + // Not completely correct for now + // Still correct for when "so" is in local + // TODO: Use the new barrier from Fred + if(PyRegion_AddRef(so, key)) { + return -1; + } so_entry->key = Py_NewRef(key); so_entry->hash = other_entry->hash; } @@ -690,8 +724,12 @@ set_merge_lock_held(PySetObject *so, PyObject *otherset) for (i = other->mask + 1; i > 0 ; i--, other_entry++) { key = other_entry->key; if (key != NULL && key != dummy) { - set_insert_clean(newtable, newmask, Py_NewRef(key), - other_entry->hash); + if(PyRegion_AddLocalRef(key)) { + return -1; + } + if(set_insert_clean(so, newtable, newmask, Py_NewRef(key),other_entry->hash)) { + return -1; + } } } return 0; @@ -855,7 +893,8 @@ setiter_dealloc(PyObject *self) setiterobject *si = (setiterobject*)self; /* bpo-31095: UnTrack is needed before calling any callbacks */ _PyObject_GC_UNTRACK(si); - Py_XDECREF(si->si_set); + PyRegion_CLEAR(si, si->si_set); + // Py_XDECREF(si->si_set); PyObject_GC_Del(si); } @@ -886,10 +925,14 @@ setiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) /* copy the iterator state */ setiterobject tmp = *si; + if(PyRegion_AddLocalRef(tmp.si_set)) { + return NULL; + } Py_XINCREF(tmp.si_set); /* iterate the temporary into a list */ PyObject *list = PySequence_List((PyObject*)&tmp); + PyRegion_RemoveLocalRef(tmp.si_set); Py_XDECREF(tmp.si_set); if (list == NULL) { return NULL; @@ -935,12 +978,15 @@ static PyObject *setiter_iternext(PyObject *self) i++; } if (i <= mask) { - key = Py_NewRef(entry[i].key); + key = PyRegion_NewRef(entry[i].key); + // key = Py_NewRef(entry[i].key); } + // Cannot return before unlocking Py_END_CRITICAL_SECTION(); si->si_pos = i+1; if (key == NULL) { si->si_set = NULL; + PyRegion_RemoveLocalRef(so); Py_DECREF(so); return NULL; } @@ -954,7 +1000,7 @@ PyTypeObject PySetIter_Type = { sizeof(setiterobject), /* tp_basicsize */ 0, /* tp_itemsize */ /* methods */ - setiter_dealloc, /* tp_dealloc */ + setiter_dealloc, /* tp_dealloc */ // DONE 0, /* tp_vectorcall_offset */ 0, /* tp_getattr */ 0, /* tp_setattr */ @@ -971,13 +1017,13 @@ PyTypeObject PySetIter_Type = { 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ 0, /* tp_doc */ - setiter_traverse, /* tp_traverse */ + setiter_traverse, /* tp_traverse */ // DONE 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ PyObject_SelfIter, /* tp_iter */ - setiter_iternext, /* tp_iternext */ - setiter_methods, /* tp_methods */ + setiter_iternext, /* tp_iternext */ // DONE + setiter_methods, /* tp_methods */ // DONE 0, }; @@ -988,6 +1034,10 @@ set_iter(PyObject *so) setiterobject *si = PyObject_GC_New(setiterobject, &PySetIter_Type); if (si == NULL) return NULL; + if(PyRegion_AddRef(si, so)) { + Py_DECREF(si); + return NULL; + } si->si_set = (PySetObject*)Py_NewRef(so); si->si_used = size; si->si_pos = 0; @@ -1040,12 +1090,16 @@ set_update_iterable_lock_held(PySetObject *so, PyObject *other) PyObject *key; while ((key = PyIter_Next(it)) != NULL) { if (set_add_key(so, key)) { + PyRegion_RemoveLocalRef(it); + PyRegion_RemoveLocalRef(key); Py_DECREF(it); Py_DECREF(key); return -1; } + PyRegion_RemoveLocalRef(key); Py_DECREF(key); } + PyRegion_RemoveLocalRef(it); Py_DECREF(it); if (PyErr_Occurred()) return -1; @@ -1079,6 +1133,7 @@ set_update_local(PySetObject *so, PyObject *other) else if (PyDict_CheckExact(other)) { int rv; Py_BEGIN_CRITICAL_SECTION(other); + // TODO: ASK FRED: using test3.py: sth goes wrong here. LRC does not increase. rv = set_update_dict_lock_held(so, other); Py_END_CRITICAL_SECTION(); return rv; @@ -1167,6 +1222,7 @@ make_new_set(PyTypeObject *type, PyObject *iterable) if (iterable != NULL) { if (set_update_local(so, iterable)) { + PyRegion_RemoveLocalRef(so); Py_DECREF(so); return NULL; } @@ -2595,12 +2651,13 @@ PyTypeObject PySet_Type = { sizeof(PySetObject), /* tp_basicsize */ 0, /* tp_itemsize */ /* methods */ - set_dealloc, /* tp_dealloc */ + set_dealloc, /* tp_dealloc */ // Done 0, /* tp_vectorcall_offset */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_as_async */ - set_repr, /* tp_repr */ + set_repr, /* tp_repr */ // Done + // TODO after set_iter: Seem to be a lot T^T &set_as_number, /* tp_as_number */ &set_as_sequence, /* tp_as_sequence */ 0, /* tp_as_mapping */ @@ -2618,7 +2675,7 @@ PyTypeObject PySet_Type = { set_clear_internal, /* tp_clear */ set_richcompare, /* tp_richcompare */ offsetof(PySetObject, weakreflist), /* tp_weaklistoffset */ - set_iter, /* tp_iter */ + set_iter, /* tp_iter */ // Done, along with setiter_***. 0, /* tp_iternext */ set_methods, /* tp_methods */ 0, /* tp_members */ @@ -2630,7 +2687,7 @@ PyTypeObject PySet_Type = { 0, /* tp_dictoffset */ set_init, /* tp_init */ PyType_GenericAlloc, /* tp_alloc */ - set_new, /* tp_new */ + set_new, /* tp_new */ // Almost Done. ASK FRED on dict case. PyObject_GC_Del, /* tp_free */ .tp_vectorcall = set_vectorcall, .tp_version_tag = _Py_TYPE_VERSION_SET, From b1358b688b963f2cdf9f708cb3cf788f599d9848 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Wed, 4 Mar 2026 10:23:15 +0100 Subject: [PATCH 11/37] update set_sub and set_and with testcases --- Objects/abstract.c | 1 + Objects/setobject.c | 103 +++++++++++++++++++++-- test_code/report_code/freeze_item.py | 8 +- test_code/set_test/setintersect_test1.py | 28 ++++++ test_code/set_test/setsub_test1.py | 65 ++++++++++++++ test_code/set_test/test2_2.py | 25 ++++++ 6 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 test_code/set_test/setintersect_test1.py create mode 100644 test_code/set_test/setsub_test1.py create mode 100644 test_code/set_test/test2_2.py diff --git a/Objects/abstract.c b/Objects/abstract.c index 234bebcb6c8a8b..11b3e96a4949aa 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -2922,6 +2922,7 @@ PyObject_GetIter(PyObject *o) PyErr_Format(PyExc_TypeError, "%T.__iter__() must return an iterator, not %T", o, res); + // ASK FRED: NO BARRIER NEEDED here, right? Py_SETREF(res, NULL); } return res; diff --git a/Objects/setobject.c b/Objects/setobject.c index 885db906bf6461..26184c1af77b17 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -111,8 +111,12 @@ set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash) return NULL; } else { // incref startkey because it can be removed from the set by the compare + if(PyRegion_AddLocalRef(startkey)) { + return NULL; + } Py_INCREF(startkey); cmp = PyObject_RichCompareBool(startkey, key, Py_EQ); + PyRegion_RemoveLocalRef(startkey); Py_DECREF(startkey); if (cmp < 0) return NULL; @@ -418,9 +422,12 @@ set_discard_entry(PySetObject *so, PyObject *key, Py_hash_t hash) if (entry->key == NULL) return DISCARD_NOTFOUND; old_key = entry->key; + // I don't think barrier should be handled for dummy entry->key = dummy; entry->hash = -1; FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used - 1); + // Use RemoveRef since old_key refers to "entry", which refers to "so" + PyRegion_RemoveRef(so, old_key); Py_DECREF(old_key); return DISCARD_FOUND; } @@ -516,6 +523,7 @@ set_clear_internal(PyObject *self) for (entry = table; used > 0; entry++) { if (entry->key && entry->key != dummy) { used--; + PyRegion_RemoveRef(so, entry->key); Py_DECREF(entry->key); } } @@ -696,16 +704,39 @@ set_merge_lock_held(PySetObject *so, PyObject *otherset) /* If our table is empty, and both tables have the same size, and there are no dummies to eliminate, then just copy the pointers. */ if (so->fill == 0 && so->mask == other->mask && other->fill == other->used) { + /* Collect keys into a temporary array for PyRegion_AddRefsArray */ + assert(other->table != NULL); + PyObject **keys_array = PyMem_New(PyObject*, other->used); + if (keys_array == NULL) { + PyErr_NoMemory(); + return -1; + } + /* First pass: collect keys */ + int keys_count = 0; + for (i = 0; i <= other->mask; i++) { + key = other->table[i].key; + if (key != NULL) { + keys_array[keys_count++] = key; + } + } + + /* Barrier before any key assignments */ + if (PyRegion_AddRefsArray(so, keys_count, keys_array)) { + PyMem_Free(keys_array); + return -1; + } + + PyMem_Free(keys_array); + + /* Reset pointers before second pass */ + so_entry = so->table; + other_entry = other->table; + + /* Second pass: copy pointers and hashes */ for (i = 0; i <= other->mask; i++, so_entry++, other_entry++) { key = other_entry->key; if (key != NULL) { assert(so_entry->key == NULL); - // Not completely correct for now - // Still correct for when "so" is in local - // TODO: Use the new barrier from Fred - if(PyRegion_AddRef(so, key)) { - return -1; - } so_entry->key = Py_NewRef(key); so_entry->hash = other_entry->hash; } @@ -1133,7 +1164,6 @@ set_update_local(PySetObject *so, PyObject *other) else if (PyDict_CheckExact(other)) { int rv; Py_BEGIN_CRITICAL_SECTION(other); - // TODO: ASK FRED: using test3.py: sth goes wrong here. LRC does not increase. rv = set_update_dict_lock_held(so, other); Py_END_CRITICAL_SECTION(); return rv; @@ -1506,20 +1536,30 @@ set_intersection(PySetObject *so, PyObject *other) while (set_next((PySetObject *)other, &pos, &entry)) { key = entry->key; hash = entry->hash; + if(PyRegion_AddLocalRef(key)) { + PyRegion_RemoveLocalRef(result); + Py_DECREF(result); + return NULL; + } Py_INCREF(key); rv = set_contains_entry(so, key, hash); if (rv < 0) { + PyRegion_RemoveLocalRef(result); + PyRegion_RemoveLocalRef(key); Py_DECREF(result); Py_DECREF(key); return NULL; } if (rv) { if (set_add_entry(result, key, hash)) { + PyRegion_RemoveLocalRef(result); + PyRegion_RemoveLocalRef(key); Py_DECREF(result); Py_DECREF(key); return NULL; } } + PyRegion_RemoveLocalRef(key); Py_DECREF(key); } return (PyObject *)result; @@ -1527,6 +1567,7 @@ set_intersection(PySetObject *so, PyObject *other) it = PyObject_GetIter(other); if (it == NULL) { + PyRegion_RemoveLocalRef(result); Py_DECREF(result); return NULL; } @@ -1542,19 +1583,26 @@ set_intersection(PySetObject *so, PyObject *other) if (set_add_entry(result, key, hash)) goto error; if (PySet_GET_SIZE(result) >= PySet_GET_SIZE(so)) { + PyRegion_RemoveLocalRef(key); Py_DECREF(key); break; } } + PyRegion_RemoveLocalRef(key); Py_DECREF(key); } + PyRegion_RemoveLocalRef(it); Py_DECREF(it); if (PyErr_Occurred()) { + PyRegion_RemoveLocalRef(result); Py_DECREF(result); return NULL; } return (PyObject *)result; error: + PyRegion_RemoveLocalRef(it); + PyRegion_RemoveLocalRef(key); + PyRegion_RemoveLocalRef(result); Py_DECREF(it); Py_DECREF(result); Py_DECREF(key); @@ -1760,20 +1808,32 @@ set_difference_update_internal(PySetObject *so, PyObject *other) if (other == NULL) return -1; } else { + if(PyRegion_AddLocalRef(other)) { + return -1; + } Py_INCREF(other); } while (set_next((PySetObject *)other, &pos, &entry)) { PyObject *key = entry->key; + if(PyRegion_AddLocalRef(key)) { + PyRegion_RemoveLocalRef(other); + Py_DECREF(other); + return -1; + } Py_INCREF(key); + // DONE Migration: Maybe? if (set_discard_entry(so, key, entry->hash) < 0) { + PyRegion_RemoveLocalRef(other); + PyRegion_RemoveLocalRef(key); Py_DECREF(other); Py_DECREF(key); return -1; } + PyRegion_RemoveLocalRef(key); Py_DECREF(key); } - + PyRegion_RemoveLocalRef(other); Py_DECREF(other); } else { PyObject *key, *it; @@ -1783,12 +1843,16 @@ set_difference_update_internal(PySetObject *so, PyObject *other) while ((key = PyIter_Next(it)) != NULL) { if (set_discard_key(so, key) < 0) { + PyRegion_RemoveLocalRef(it); + PyRegion_RemoveLocalRef(key); Py_DECREF(it); Py_DECREF(key); return -1; } + PyRegion_RemoveLocalRef(key); Py_DECREF(key); } + PyRegion_RemoveLocalRef(it); Py_DECREF(it); if (PyErr_Occurred()) return -1; @@ -1837,6 +1901,7 @@ set_copy_and_difference(PySetObject *so, PyObject *other) return NULL; if (set_difference_update_internal((PySetObject *) result, other) == 0) return result; + assert(PyRegion_IsLocal(result)); Py_DECREF(result); return NULL; } @@ -1875,20 +1940,30 @@ set_difference(PySetObject *so, PyObject *other) while (set_next(so, &pos, &entry)) { key = entry->key; hash = entry->hash; + if(PyRegion_AddLocalRef(key)) { + PyRegion_RemoveLocalRef(result); + Py_DECREF(result); + return NULL; + } Py_INCREF(key); rv = _PyDict_Contains_KnownHash(other, key, hash); if (rv < 0) { + PyRegion_RemoveLocalRef(result); + PyRegion_RemoveLocalRef(key); Py_DECREF(result); Py_DECREF(key); return NULL; } if (!rv) { if (set_add_entry((PySetObject *)result, key, hash)) { + PyRegion_RemoveLocalRef(result); + PyRegion_RemoveLocalRef(key); Py_DECREF(result); Py_DECREF(key); return NULL; } } + PyRegion_RemoveLocalRef(key); Py_DECREF(key); } return result; @@ -1898,20 +1973,30 @@ set_difference(PySetObject *so, PyObject *other) while (set_next(so, &pos, &entry)) { key = entry->key; hash = entry->hash; + if(PyRegion_AddLocalRef(key)) { + PyRegion_RemoveLocalRef(result); + Py_DECREF(result); + return NULL; + } Py_INCREF(key); rv = set_contains_entry((PySetObject *)other, key, hash); if (rv < 0) { + PyRegion_RemoveLocalRef(result); + PyRegion_RemoveLocalRef(key); Py_DECREF(result); Py_DECREF(key); return NULL; } if (!rv) { if (set_add_entry((PySetObject *)result, key, hash)) { + PyRegion_RemoveLocalRef(result); + PyRegion_RemoveLocalRef(key); Py_DECREF(result); Py_DECREF(key); return NULL; } } + PyRegion_RemoveLocalRef(key); Py_DECREF(key); } return result; @@ -2622,6 +2707,7 @@ static PyNumberMethods set_as_number = { 0, /*nb_lshift*/ 0, /*nb_rshift*/ set_and, /*nb_and*/ + // TODO set_xor, /*nb_xor*/ set_or, /*nb_or*/ 0, /*nb_int*/ @@ -2726,6 +2812,7 @@ static PyNumberMethods frozenset_as_number = { 0, /*nb_invert*/ 0, /*nb_lshift*/ 0, /*nb_rshift*/ + // TODO set_and, /*nb_and*/ set_xor, /*nb_xor*/ set_or, /*nb_or*/ diff --git a/test_code/report_code/freeze_item.py b/test_code/report_code/freeze_item.py index 8c771d0b6df2f1..6762d86ddf08f0 100644 --- a/test_code/report_code/freeze_item.py +++ b/test_code/report_code/freeze_item.py @@ -1,5 +1,5 @@ from regions import Region, is_local -from immutable import freeze, register_freezable +from immutable import freeze, register_freezable, isfrozen r = Region() class A: pass @@ -9,7 +9,11 @@ class A: pass print(f"Region after setting start: {r}") print(f"Obj is in the local region: {is_local(obj)}") print(f"Region owns obj: {r.owns(obj)}") +print(f"{isfrozen(obj)}") +print(f"{isfrozen(obj['key'])}") freeze(obj) print(f"Region after freezing obj: {r}") print(f"Obj is in the local region: {is_local(obj)}") -print(f"Region owns obj: {r.owns(obj)}") \ No newline at end of file +print(f"Region owns obj: {r.owns(obj)}") +print(f"{isfrozen(obj)}") +print(f"{isfrozen(obj['key'])}") \ No newline at end of file diff --git a/test_code/set_test/setintersect_test1.py b/test_code/set_test/setintersect_test1.py new file mode 100644 index 00000000000000..520592ba919c19 --- /dev/null +++ b/test_code/set_test/setintersect_test1.py @@ -0,0 +1,28 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() +r.arr1 = [r.a, r.b, r.c] +r.arr2 = [r.b, r.c, r.f] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") +print(f"{s2}") +input("Press Enter to create set intersection...") +result = s1 & s2 +print(f"Region after creating set intersection: {r}") +print(f"Intersection result: {result}") \ No newline at end of file diff --git a/test_code/set_test/setsub_test1.py b/test_code/set_test/setsub_test1.py new file mode 100644 index 00000000000000..9d601d25579c1b --- /dev/null +++ b/test_code/set_test/setsub_test1.py @@ -0,0 +1,65 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r2.d = A() +r2.e = A() +f = A() +g = A() + +r.arr1 = [r.a, r.b, r.c] +r.arr2 = [r.a] +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") +# print(f"{s1}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") +# print(f"{s2}") +# input("Press Enter to create set difference...") +# s3 = s1-s2 +# print(f"Region after creating set difference: {r}") +# print(f"{s3}") + +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") +# ss1 = set(s1) +# print(f"Region after creating set from set1: {r}") +# ss2 = set(s2) +# print(f"Region after creating set from set2: {r}") +# ss3 = ss1-ss2 +# print(f"Region after creating set difference of set: {r}") + +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") +# print(f"{s1}") +# arr2 = [r.a] +# print(f"Region after creating set2: {r}") +# print(f"{arr2}") +# input("Press Enter to create set difference...") +# s3 = s1.difference(arr2) +# print(f"Region after creating set difference: {r}") +# print(f"{s3}") + +arr3 = [r.a, r.b, r.c, r2.d] +print(f"Region1 after creating arr3: {r}") +print(f"Region2 after creating arr3: {r2}") +s1 = set(arr3) +print(f"Region after creating set1: {r}") +print(f"Region2 after creating set1: {r2}") +print(f"{s1}") +input("Press Enter to create set difference...") +s3 = s1.difference(r.arr1) +print(f"Region after creating set difference: {r}") +print(f"Region2 after creating set difference: {r2}") +print(f"{s3}") \ No newline at end of file diff --git a/test_code/set_test/test2_2.py b/test_code/set_test/test2_2.py new file mode 100644 index 00000000000000..e813a53e2acb37 --- /dev/null +++ b/test_code/set_test/test2_2.py @@ -0,0 +1,25 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +aa=A() +ab=A() +ac=A() +r2.word4=A() +# TODO: Test undo when 3 are in local, 1 is in another region +arr = [aa, ab, ac, r2.word4] +print(f"Region after setting word: {r}") # LRC=2 +s = set(arr) +print(f"Region after creating set: {r}") # LRC=6 since it borrows word, word2, word3, and word4. "s" does not matter here since set is not in the region yet. +try: + r.set = s +except Exception as e: + print(f"Error when moving set into the region: {e}") + print(f"{is_local(aa)}, {is_local(ab)}, {is_local(ac)}") + print(f"Region after moving set into the region: {r}") From 4c8910d28764566c68a9581f73a856e1fb0f2b8e Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Wed, 4 Mar 2026 17:20:27 +0100 Subject: [PATCH 12/37] finish migrating set_as_number --- Objects/setobject.c | 67 +++++++++++++++---- test_code/set_test/setand_test1.py | 45 +++++++++++++ test_code/set_test/setiand_test1.py | 29 ++++++++ test_code/set_test/setior_test1.py | 55 +++++++++++++++ test_code/set_test/setissubset_test1.py | 57 ++++++++++++++++ test_code/set_test/setissuperset_test1.py | 57 ++++++++++++++++ test_code/set_test/setisub_test1.py | 41 ++++++++++++ test_code/set_test/setixor_test1.py | 55 +++++++++++++++ .../{setintersect_test1.py => setor_test1.py} | 9 +-- test_code/set_test/setsub_test1.py | 48 +++++++------ test_code/set_test/setxor_test1.py | 55 +++++++++++++++ 11 files changed, 480 insertions(+), 38 deletions(-) create mode 100644 test_code/set_test/setand_test1.py create mode 100644 test_code/set_test/setiand_test1.py create mode 100644 test_code/set_test/setior_test1.py create mode 100644 test_code/set_test/setissubset_test1.py create mode 100644 test_code/set_test/setissuperset_test1.py create mode 100644 test_code/set_test/setisub_test1.py create mode 100644 test_code/set_test/setixor_test1.py rename test_code/set_test/{setintersect_test1.py => setor_test1.py} (72%) create mode 100644 test_code/set_test/setxor_test1.py diff --git a/Objects/setobject.c b/Objects/setobject.c index 26184c1af77b17..03c49c269696b9 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1396,6 +1396,7 @@ set_copy_impl(PySetObject *so) return NULL; } if (set_merge_lock_held((PySetObject *)copy, (PyObject *)so) < 0) { + PyRegion_RemoveLocalRef(copy); Py_DECREF(copy); return NULL; } @@ -1466,6 +1467,7 @@ set_union_impl(PySetObject *so, PyObject * const *others, if ((PyObject *)so == other) continue; if (set_update_local(result, other)) { + PyRegion_RemoveLocalRef(result); Py_DECREF(result); return NULL; } @@ -1489,6 +1491,7 @@ set_or(PyObject *self, PyObject *other) return (PyObject *)result; } if (set_update_local(result, other)) { + PyRegion_RemoveLocalRef(result); Py_DECREF(result); return NULL; } @@ -1505,7 +1508,7 @@ set_ior(PyObject *self, PyObject *other) if (set_update_internal(so, other)) { return NULL; } - return Py_NewRef(so); + return PyRegion_NewRef(so); } static PyObject * @@ -1636,10 +1639,12 @@ set_intersection_multi_impl(PySetObject *so, PyObject * const *others, newresult = set_intersection((PySetObject *)result, other); Py_END_CRITICAL_SECTION2(); if (newresult == NULL) { + PyRegion_RemoveLocalRef(result); Py_DECREF(result); return NULL; } - Py_SETREF(result, newresult); + PyRegion_XSETLOCALREF(result, newresult); + // Py_SETREF(result, newresult); } return result; } @@ -1653,6 +1658,7 @@ set_intersection_update(PySetObject *so, PyObject *other) if (tmp == NULL) return NULL; set_swap_bodies(so, (PySetObject *)tmp); + PyRegion_RemoveLocalRef(tmp); Py_DECREF(tmp); Py_RETURN_NONE; } @@ -1678,6 +1684,7 @@ set_intersection_update_multi_impl(PySetObject *so, PyObject * const *others, Py_BEGIN_CRITICAL_SECTION(so); set_swap_bodies(so, (PySetObject *)tmp); Py_END_CRITICAL_SECTION(); + PyRegion_RemoveLocalRef(tmp); Py_DECREF(tmp); Py_RETURN_NONE; } @@ -1712,8 +1719,9 @@ set_iand(PyObject *self, PyObject *other) if (result == NULL) return NULL; + PyRegion_RemoveLocalRef(result); Py_DECREF(result); - return Py_NewRef(so); + return PyRegion_NewRef(so); } /*[clinic input] @@ -1751,8 +1759,12 @@ set_isdisjoint_impl(PySetObject *so, PyObject *other) } while (set_next((PySetObject *)other, &pos, &entry)) { PyObject *key = entry->key; + if(PyRegion_AddLocalRef(key)) { + return NULL; + } Py_INCREF(key); rv = set_contains_entry(so, key, entry->hash); + PyRegion_RemoveLocalRef(key); Py_DECREF(key); if (rv < 0) { return NULL; @@ -1770,16 +1782,20 @@ set_isdisjoint_impl(PySetObject *so, PyObject *other) while ((key = PyIter_Next(it)) != NULL) { rv = set_contains_key(so, key); + PyRegion_RemoveLocalRef(key); Py_DECREF(key); if (rv < 0) { + PyRegion_RemoveLocalRef(it); Py_DECREF(it); return NULL; } if (rv) { + PyRegion_RemoveLocalRef(it); Py_DECREF(it); Py_RETURN_FALSE; } } + PyRegion_RemoveLocalRef(it); Py_DECREF(it); if (PyErr_Occurred()) return NULL; @@ -2036,6 +2052,7 @@ set_difference_multi_impl(PySetObject *so, PyObject * const *others, rv = set_difference_update_internal((PySetObject *)result, other); Py_END_CRITICAL_SECTION(); if (rv) { + assert(PyRegion_IsLocal(result)); Py_DECREF(result); return NULL; } @@ -2071,7 +2088,7 @@ set_isub(PyObject *self, PyObject *other) if (rv < 0) { return NULL; } - return Py_NewRef(so); + return PyRegion_NewRef(so); } static int @@ -2084,18 +2101,24 @@ set_symmetric_difference_update_dict(PySetObject *so, PyObject *other) PyObject *key, *value; Py_hash_t hash; while (_PyDict_Next(other, &pos, &key, &value, &hash)) { + if(PyRegion_AddLocalRef(key)) { + return -1; + } Py_INCREF(key); int rv = set_discard_entry(so, key, hash); if (rv < 0) { + PyRegion_RemoveLocalRef(key); Py_DECREF(key); return -1; } if (rv == DISCARD_NOTFOUND) { if (set_add_entry(so, key, hash)) { + PyRegion_RemoveLocalRef(key); Py_DECREF(key); return -1; } } + PyRegion_RemoveLocalRef(key); Py_DECREF(key); } return 0; @@ -2110,19 +2133,25 @@ set_symmetric_difference_update_set(PySetObject *so, PySetObject *other) Py_ssize_t pos = 0; setentry *entry; while (set_next(other, &pos, &entry)) { - PyObject *key = Py_NewRef(entry->key); + PyObject *key = PyRegion_NewRef(entry->key); + if(key == NULL) { + return -1; + } Py_hash_t hash = entry->hash; int rv = set_discard_entry(so, key, hash); if (rv < 0) { + PyRegion_RemoveLocalRef(key); Py_DECREF(key); return -1; } if (rv == DISCARD_NOTFOUND) { if (set_add_entry(so, key, hash)) { + PyRegion_RemoveLocalRef(key); Py_DECREF(key); return -1; } } + PyRegion_RemoveLocalRef(key); Py_DECREF(key); } return 0; @@ -2167,6 +2196,7 @@ set_symmetric_difference_update_impl(PySetObject *so, PyObject *other) rv = set_symmetric_difference_update_set(so, otherset); Py_END_CRITICAL_SECTION(); + PyRegion_RemoveLocalRef(otherset); Py_DECREF(otherset); } if (rv < 0) { @@ -2194,10 +2224,12 @@ set_symmetric_difference_impl(PySetObject *so, PyObject *other) return NULL; } if (set_update_lock_held(result, other) < 0) { + PyRegion_RemoveLocalRef(result); Py_DECREF(result); return NULL; } if (set_symmetric_difference_update_set(result, so) < 0) { + PyRegion_RemoveLocalRef(result); Py_DECREF(result); return NULL; } @@ -2225,8 +2257,9 @@ set_ixor(PyObject *self, PyObject *other) result = set_symmetric_difference_update((PyObject*)so, other); if (result == NULL) return NULL; + PyRegion_RemoveLocalRef(result); Py_DECREF(result); - return Py_NewRef(so); + return PyRegion_NewRef(so); } /*[clinic input] @@ -2253,6 +2286,7 @@ set_issubset_impl(PySetObject *so, PyObject *other) return NULL; } int result = (PySet_GET_SIZE(tmp) == PySet_GET_SIZE(so)); + PyRegion_RemoveLocalRef(tmp); Py_DECREF(tmp); return PyBool_FromLong(result); } @@ -2261,8 +2295,12 @@ set_issubset_impl(PySetObject *so, PyObject *other) while (set_next(so, &pos, &entry)) { PyObject *key = entry->key; + if(PyRegion_AddLocalRef(key)) { + return NULL; + } Py_INCREF(key); rv = set_contains_entry((PySetObject *)other, key, entry->hash); + PyRegion_RemoveLocalRef(key); Py_DECREF(key); if (rv < 0) { return NULL; @@ -2298,16 +2336,20 @@ set_issuperset_impl(PySetObject *so, PyObject *other) } while ((key = PyIter_Next(it)) != NULL) { int rv = set_contains_key(so, key); + PyRegion_RemoveLocalRef(key); Py_DECREF(key); if (rv < 0) { + PyRegion_RemoveLocalRef(it); Py_DECREF(it); return NULL; } if (!rv) { + PyRegion_AddLocalRef(it); Py_DECREF(it); Py_RETURN_FALSE; } } + PyRegion_RemoveLocalRef(it); Py_DECREF(it); if (PyErr_Occurred()) { return NULL; @@ -2578,6 +2620,9 @@ set___reduce___impl(PySetObject *so) goto done; result = PyTuple_Pack(3, Py_TYPE(so), args, state); done: + assert(PyRegion_IsLocal(args) || args == NULL); + assert(PyRegion_IsLocal(keys) || keys == NULL); + assert(PyRegion_IsLocal(state) || state == NULL); Py_XDECREF(args); Py_XDECREF(keys); Py_XDECREF(state); @@ -2707,7 +2752,6 @@ static PyNumberMethods set_as_number = { 0, /*nb_lshift*/ 0, /*nb_rshift*/ set_and, /*nb_and*/ - // TODO set_xor, /*nb_xor*/ set_or, /*nb_or*/ 0, /*nb_int*/ @@ -2745,7 +2789,7 @@ PyTypeObject PySet_Type = { set_repr, /* tp_repr */ // Done // TODO after set_iter: Seem to be a lot T^T &set_as_number, /* tp_as_number */ - &set_as_sequence, /* tp_as_sequence */ + &set_as_sequence, /* tp_as_sequence */ // Done 0, /* tp_as_mapping */ PyObject_HashNotImplemented, /* tp_hash */ 0, /* tp_call */ @@ -2757,8 +2801,8 @@ PyTypeObject PySet_Type = { Py_TPFLAGS_BASETYPE | _Py_TPFLAGS_MATCH_SELF, /* tp_flags */ set_doc, /* tp_doc */ - set_traverse, /* tp_traverse */ - set_clear_internal, /* tp_clear */ + set_traverse, /* tp_traverse */ // Done + set_clear_internal, /* tp_clear */ // Done set_richcompare, /* tp_richcompare */ offsetof(PySetObject, weakreflist), /* tp_weaklistoffset */ set_iter, /* tp_iter */ // Done, along with setiter_***. @@ -2773,7 +2817,7 @@ PyTypeObject PySet_Type = { 0, /* tp_dictoffset */ set_init, /* tp_init */ PyType_GenericAlloc, /* tp_alloc */ - set_new, /* tp_new */ // Almost Done. ASK FRED on dict case. + set_new, /* tp_new */ // Done PyObject_GC_Del, /* tp_free */ .tp_vectorcall = set_vectorcall, .tp_version_tag = _Py_TYPE_VERSION_SET, @@ -2812,7 +2856,6 @@ static PyNumberMethods frozenset_as_number = { 0, /*nb_invert*/ 0, /*nb_lshift*/ 0, /*nb_rshift*/ - // TODO set_and, /*nb_and*/ set_xor, /*nb_xor*/ set_or, /*nb_or*/ diff --git a/test_code/set_test/setand_test1.py b/test_code/set_test/setand_test1.py new file mode 100644 index 00000000000000..824372d6dada03 --- /dev/null +++ b/test_code/set_test/setand_test1.py @@ -0,0 +1,45 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() +r.arr1 = [r.a, r.b, r.c] +r.arr2 = [r.b, r.c, r.f] +r.arr3 = [r.b, r.d, r.e] + +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") +print(f"{s2}") +input("Press Enter to create set intersection...") +# result = s1 & s2 +result = s1.intersection(s2) +print(f"Region after creating set intersection: {r}") +print(f"Intersection result: {result}") + +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") +# print(f"{s1}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") +# print(f"{s2}") +# s3 = set(r.arr3) +# print(f"Region after creating set3: {r}") +# print(f"{s3}") +# input("Press Enter to create set intersection...") +# result = s1.intersection(s2, s3) +# print(f"Region after creating set intersection: {r}") +# print(f"Intersection result: {result}") \ No newline at end of file diff --git a/test_code/set_test/setiand_test1.py b/test_code/set_test/setiand_test1.py new file mode 100644 index 00000000000000..cedc9f8b7eacae --- /dev/null +++ b/test_code/set_test/setiand_test1.py @@ -0,0 +1,29 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() +r.arr1 = [r.a, r.b, r.c] +r.arr2 = [r.b, r.c, r.f] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") # +3 +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") # +3 +print(f"{s2}") +input("Press Enter to create set intersection...") +# s1 &= s2 +s1.intersection_update(s2) +print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1, then +2: add the reference from s1 to the result +print(f"Intersection result: {s1}") \ No newline at end of file diff --git a/test_code/set_test/setior_test1.py b/test_code/set_test/setior_test1.py new file mode 100644 index 00000000000000..8c7b495f742b94 --- /dev/null +++ b/test_code/set_test/setior_test1.py @@ -0,0 +1,55 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() +r.arr1 = [r.a, r.b, r.c] +r.arr2 = [r.b, r.c, r.f] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") +print(f"{s2}") +input("Press Enter to create set or...") +s1 |= s2 +# result = s1.symmetric_difference(s2) +print(f"Region after creating set or: {r}") +print(f"Or result: {s1}") + +# r.a = A() +# r.b = A() +# r.c = A() +# r2.d = A() +# r2.e = A() +# r2.f = A() +# r2.g = A() +# arr1 = [r.a, r.b, r.c, r2.d] +# arr2 = [r.c, r2.d, r2.e, r2.f, r2.g] +# print(f"Region1 before creating sets: {r}") +# print(f"Region2 before creating sets: {r2}") +# s1 = set(arr1) +# print(f"Region1 after creating set1: {r}") +# print(f"Region2 after creating set1: {r2}") +# print(f"{s1}") +# s2 = set(arr2) +# print(f"Region1 after creating set2: {r}") +# print(f"Region2 after creating set2: {r2}") +# print(f"{s2}") +# input("Press Enter to create set xor...") +# result = s1 ^ s2 +# # result = s1.symmetric_difference(s2) +# print(f"Region1 after creating set xor: {r}") +# print(f"Region2 after creating set xor: {r2}") +# print(f"Xor result: {result}") \ No newline at end of file diff --git a/test_code/set_test/setissubset_test1.py b/test_code/set_test/setissubset_test1.py new file mode 100644 index 00000000000000..38f78816ee8704 --- /dev/null +++ b/test_code/set_test/setissubset_test1.py @@ -0,0 +1,57 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() +r.arr1 = [r.a] +r.arr2 = [r.a, r.b, r.c] +r.arr3 = [r.a, r.b, r.c, r.d, r.e, r.f] +r.arr4 = [r.d] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") +print(f"{s2}") +input("Press Enter to issubset...") +# result = s1 <= s2 +result = s1.issubset(s2) +print(f"Region after creating set issubset: {r}") +print(f"IsSubset result: {result}") + +# r.a = A() +# r.b = A() +# r.c = A() +# r2.d = A() +# r2.e = A() +# r2.f = A() +# r2.g = A() +# arr1 = [r.a, r.b, r.c, r2.d] +# arr2 = [r.c, r2.d, r2.e, r2.f, r2.g] +# print(f"Region1 before creating sets: {r}") +# print(f"Region2 before creating sets: {r2}") +# s1 = set(arr1) +# print(f"Region1 after creating set1: {r}") +# print(f"Region2 after creating set1: {r2}") +# print(f"{s1}") +# s2 = set(arr2) +# print(f"Region1 after creating set2: {r}") +# print(f"Region2 after creating set2: {r2}") +# print(f"{s2}") +# input("Press Enter to create set xor...") +# result = s1 ^ s2 +# # result = s1.symmetric_difference(s2) +# print(f"Region1 after creating set xor: {r}") +# print(f"Region2 after creating set xor: {r2}") +# print(f"Xor result: {result}") \ No newline at end of file diff --git a/test_code/set_test/setissuperset_test1.py b/test_code/set_test/setissuperset_test1.py new file mode 100644 index 00000000000000..9a9aa334f36cbe --- /dev/null +++ b/test_code/set_test/setissuperset_test1.py @@ -0,0 +1,57 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() +r.arr1 = [r.a] +r.arr2 = [r.a, r.b, r.c] +r.arr3 = [r.a, r.b, r.c, r.d, r.e, r.f] +r.arr4 = [r.d] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") +print(f"{s1}") +s2 = set(r.arr1) +print(f"Region after creating set2: {r}") +print(f"{s2}") +input("Press Enter to issuperset...") +result = s2 > s1 +# result = s2.issuperset(s1) +print(f"Region after creating set issuperset: {r}") +print(f"IsSuperset result: {result}") + +# r.a = A() +# r.b = A() +# r.c = A() +# r2.d = A() +# r2.e = A() +# r2.f = A() +# r2.g = A() +# arr1 = [r.a, r.b, r.c, r2.d] +# arr2 = [r.c, r2.d, r2.e, r2.f, r2.g] +# print(f"Region1 before creating sets: {r}") +# print(f"Region2 before creating sets: {r2}") +# s1 = set(arr1) +# print(f"Region1 after creating set1: {r}") +# print(f"Region2 after creating set1: {r2}") +# print(f"{s1}") +# s2 = set(arr2) +# print(f"Region1 after creating set2: {r}") +# print(f"Region2 after creating set2: {r2}") +# print(f"{s2}") +# input("Press Enter to create set xor...") +# result = s1 ^ s2 +# # result = s1.symmetric_difference(s2) +# print(f"Region1 after creating set xor: {r}") +# print(f"Region2 after creating set xor: {r2}") +# print(f"Xor result: {result}") \ No newline at end of file diff --git a/test_code/set_test/setisub_test1.py b/test_code/set_test/setisub_test1.py new file mode 100644 index 00000000000000..981afbb0d866cc --- /dev/null +++ b/test_code/set_test/setisub_test1.py @@ -0,0 +1,41 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r2.d = A() +r2.e = A() +f = A() +g = A() + +r.arr1 = [r.a, r.b, r.c] +r.arr2 = [r.a] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") +print(f"{s2}") +input("Press Enter to create set difference...") +s1 -= s2 +print(f"Region after creating set difference: {r}") +print(f"{s1}") + +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") +# ss1 = set(s1) +# print(f"Region after creating set from set1: {r}") +# ss2 = set(s2) +# print(f"Region after creating set from set2: {r}") +# ss1 -= ss2 +# print(f"Region after creating set difference of set: {r}") diff --git a/test_code/set_test/setixor_test1.py b/test_code/set_test/setixor_test1.py new file mode 100644 index 00000000000000..f05058ee659aeb --- /dev/null +++ b/test_code/set_test/setixor_test1.py @@ -0,0 +1,55 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() +r.arr1 = [r.a, r.b, r.c, r.f] +r.arr2 = [r.a, r.b, r.f] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") # +4 +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") # +3 +print(f"{s2}") +input("Press Enter to create set xor...") +# s1 ^= s2 +s1.symmetric_difference_update(s2) # -4 for derefs r.a, r.b, r.c, r.f, then +1 for r.c +print(f"Region after creating set xor: {r}") +print(f"Xor result: {s1}") + +# r.a = A() +# r.b = A() +# r.c = A() +# r2.d = A() +# r2.e = A() +# r2.f = A() +# r2.g = A() +# arr1 = [r.a, r.b, r.c, r2.d] +# arr2 = [r.c, r2.d, r2.e, r2.f, r2.g] +# print(f"Region1 before creating sets: {r}") +# print(f"Region2 before creating sets: {r2}") +# s1 = set(arr1) +# print(f"Region1 after creating set1: {r}") +# print(f"Region2 after creating set1: {r2}") +# print(f"{s1}") +# s2 = set(arr2) +# print(f"Region1 after creating set2: {r}") +# print(f"Region2 after creating set2: {r2}") +# print(f"{s2}") +# input("Press Enter to create set xor...") +# s1 ^= s2 +# # result = s1.symmetric_difference(s2) +# print(f"Region1 after creating set xor: {r}") +# print(f"Region2 after creating set xor: {r2}") +# print(f"Xor result: {s1}") \ No newline at end of file diff --git a/test_code/set_test/setintersect_test1.py b/test_code/set_test/setor_test1.py similarity index 72% rename from test_code/set_test/setintersect_test1.py rename to test_code/set_test/setor_test1.py index 520592ba919c19..970ef42d7ff3d2 100644 --- a/test_code/set_test/setintersect_test1.py +++ b/test_code/set_test/setor_test1.py @@ -22,7 +22,8 @@ class A: pass; s2 = set(r.arr2) print(f"Region after creating set2: {r}") print(f"{s2}") -input("Press Enter to create set intersection...") -result = s1 & s2 -print(f"Region after creating set intersection: {r}") -print(f"Intersection result: {result}") \ No newline at end of file +input("Press Enter to create set or...") +result = s1 | s2 +# result = s1.union(s2) +print(f"Region after creating set or: {r}") +print(f"Or result: {result}") \ No newline at end of file diff --git a/test_code/set_test/setsub_test1.py b/test_code/set_test/setsub_test1.py index 9d601d25579c1b..e4e1416e60aa2e 100644 --- a/test_code/set_test/setsub_test1.py +++ b/test_code/set_test/setsub_test1.py @@ -18,16 +18,20 @@ class A: pass; r.arr1 = [r.a, r.b, r.c] r.arr2 = [r.a] -# s1 = set(r.arr1) -# print(f"Region after creating set1: {r}") -# print(f"{s1}") -# s2 = set(r.arr2) -# print(f"Region after creating set2: {r}") -# print(f"{s2}") -# input("Press Enter to create set difference...") -# s3 = s1-s2 -# print(f"Region after creating set difference: {r}") -# print(f"{s3}") +r.arr3 = [r.b] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") +print(f"{s2}") +s3 = set(r.arr3) +print(f"Region after creating set3: {r}") +print(f"{s3}") +input("Press Enter to create set difference...") +s4 = s1.difference(s2, s3) +print(f"Region after creating set difference: {r}") +print(f"{s4}") # s1 = set(r.arr1) # print(f"Region after creating set1: {r}") @@ -51,15 +55,15 @@ class A: pass; # print(f"Region after creating set difference: {r}") # print(f"{s3}") -arr3 = [r.a, r.b, r.c, r2.d] -print(f"Region1 after creating arr3: {r}") -print(f"Region2 after creating arr3: {r2}") -s1 = set(arr3) -print(f"Region after creating set1: {r}") -print(f"Region2 after creating set1: {r2}") -print(f"{s1}") -input("Press Enter to create set difference...") -s3 = s1.difference(r.arr1) -print(f"Region after creating set difference: {r}") -print(f"Region2 after creating set difference: {r2}") -print(f"{s3}") \ No newline at end of file +# arr3 = [r.a, r.b, r.c, r2.d] +# print(f"Region1 after creating arr3: {r}") +# print(f"Region2 after creating arr3: {r2}") +# s1 = set(arr3) +# print(f"Region after creating set1: {r}") +# print(f"Region2 after creating set1: {r2}") +# print(f"{s1}") +# input("Press Enter to create set difference...") +# s3 = s1.difference(r.arr1) +# print(f"Region after creating set difference: {r}") +# print(f"Region2 after creating set difference: {r2}") +# print(f"{s3}") \ No newline at end of file diff --git a/test_code/set_test/setxor_test1.py b/test_code/set_test/setxor_test1.py new file mode 100644 index 00000000000000..ae8421e2126ae6 --- /dev/null +++ b/test_code/set_test/setxor_test1.py @@ -0,0 +1,55 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +r2 = Region() +print(f"Initial Region: {r}") + +class A: pass; +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() +r.arr1 = [r.a, r.b, r.c] +r.arr2 = [r.b, r.c, r.f] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") +print(f"{s2}") +input("Press Enter to create set xor...") +# result = s1 ^ s2 +result = s1.symmetric_difference(s2) +print(f"Region after creating set xor: {r}") +print(f"Xor result: {result}") + +# r.a = A() +# r.b = A() +# r.c = A() +# r2.d = A() +# r2.e = A() +# r2.f = A() +# r2.g = A() +# arr1 = [r.a, r.b, r.c, r2.d] +# arr2 = [r.c, r2.d, r2.e, r2.f, r2.g] +# print(f"Region1 before creating sets: {r}") +# print(f"Region2 before creating sets: {r2}") +# s1 = set(arr1) +# print(f"Region1 after creating set1: {r}") +# print(f"Region2 after creating set1: {r2}") +# print(f"{s1}") +# s2 = set(arr2) +# print(f"Region1 after creating set2: {r}") +# print(f"Region2 after creating set2: {r2}") +# print(f"{s2}") +# input("Press Enter to create set xor...") +# result = s1 ^ s2 +# # result = s1.symmetric_difference(s2) +# print(f"Region1 after creating set xor: {r}") +# print(f"Region2 after creating set xor: {r2}") +# print(f"Xor result: {result}") \ No newline at end of file From dad52e4d7f0eec6adef7a0f5debb823f8e997702 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Thu, 5 Mar 2026 13:44:23 +0100 Subject: [PATCH 13/37] migrate more set data structure --- Objects/setobject.c | 20 ++++++------- .../set_test/frozenset_create_delete_1.py | 29 +++++++++++++++++++ test_code/set_test/setcopy_test1.py | 21 ++++++++++++++ test_code/set_test/setpopdiscard_test1.py | 29 +++++++++++++++++++ 4 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 test_code/set_test/frozenset_create_delete_1.py create mode 100644 test_code/set_test/setcopy_test1.py create mode 100644 test_code/set_test/setpopdiscard_test1.py diff --git a/Objects/setobject.c b/Objects/setobject.c index 03c49c269696b9..dfc39f82bc6558 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1282,7 +1282,7 @@ make_new_frozenset(PyTypeObject *type, PyObject *iterable) if (iterable != NULL && PyFrozenSet_CheckExact(iterable)) { /* frozenset(f) is idempotent */ - return Py_NewRef(iterable); + return PyRegion_NewRef(iterable); } return make_new_set(type, iterable); } @@ -1416,7 +1416,7 @@ frozenset_copy_impl(PySetObject *so) /*[clinic end generated code: output=b356263526af9e70 input=fbf5bef131268dd7]*/ { if (PyFrozenSet_CheckExact(so)) { - return Py_NewRef(so); + return PyRegion_NewRef(so); } return set_copy_impl(so); } @@ -2381,6 +2381,7 @@ set_richcompare(PyObject *self, PyObject *w, int op) if (r1 == NULL) return NULL; r2 = PyObject_IsTrue(r1); + PyRegion_RemoveLocalRef(r1); Py_DECREF(r1); if (r2 < 0) return NULL; @@ -2787,8 +2788,7 @@ PyTypeObject PySet_Type = { 0, /* tp_setattr */ 0, /* tp_as_async */ set_repr, /* tp_repr */ // Done - // TODO after set_iter: Seem to be a lot T^T - &set_as_number, /* tp_as_number */ + &set_as_number, /* tp_as_number */ // Done &set_as_sequence, /* tp_as_sequence */ // Done 0, /* tp_as_mapping */ PyObject_HashNotImplemented, /* tp_hash */ @@ -2803,7 +2803,7 @@ PyTypeObject PySet_Type = { set_doc, /* tp_doc */ set_traverse, /* tp_traverse */ // Done set_clear_internal, /* tp_clear */ // Done - set_richcompare, /* tp_richcompare */ + set_richcompare, /* tp_richcompare */ // Done offsetof(PySetObject, weakreflist), /* tp_weaklistoffset */ set_iter, /* tp_iter */ // Done, along with setiter_***. 0, /* tp_iternext */ @@ -2815,7 +2815,7 @@ PyTypeObject PySet_Type = { 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ - set_init, /* tp_init */ + set_init, /* tp_init */ // Haven't tested yet, but it should be fine. PyType_GenericAlloc, /* tp_alloc */ set_new, /* tp_new */ // Done PyObject_GC_Del, /* tp_free */ @@ -2879,8 +2879,8 @@ PyTypeObject PyFrozenSet_Type = { 0, /* tp_setattr */ 0, /* tp_as_async */ set_repr, /* tp_repr */ - &frozenset_as_number, /* tp_as_number */ - &set_as_sequence, /* tp_as_sequence */ + &frozenset_as_number, /* tp_as_number */ // Done + &set_as_sequence, /* tp_as_sequence */ // Done 0, /* tp_as_mapping */ frozenset_hash, /* tp_hash */ 0, /* tp_call */ @@ -2889,8 +2889,8 @@ PyTypeObject PyFrozenSet_Type = { 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | - Py_TPFLAGS_BASETYPE | - _Py_TPFLAGS_MATCH_SELF, /* tp_flags */ + Py_TPFLAGS_BASETYPE | + _Py_TPFLAGS_MATCH_SELF, /* tp_flags */ frozenset_doc, /* tp_doc */ set_traverse, /* tp_traverse */ set_clear_internal, /* tp_clear */ diff --git a/test_code/set_test/frozenset_create_delete_1.py b/test_code/set_test/frozenset_create_delete_1.py new file mode 100644 index 00000000000000..ed173148fb9698 --- /dev/null +++ b/test_code/set_test/frozenset_create_delete_1.py @@ -0,0 +1,29 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +print(f"Initial Region: {r}") +class A: pass +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() + +r.arr1 = [r.a, r.b, r.c] +print(f"Region after setting arr1: {r}") +s1 = frozenset(r.arr1) +print(f"Region after creating frozenset s1: {r}") +r.set1 = s1 +print(f"Region after setting set1: {r}") + +# s2 = frozenset(s1) +# print(f"Region after creating frozenset s2: {r}") +# print(f"{s2 is r.set1}") + +s2 = s1.copy() +print(f"Region after copying s1 to s2: {r}") +print(f"{s2 is r.set1}") \ No newline at end of file diff --git a/test_code/set_test/setcopy_test1.py b/test_code/set_test/setcopy_test1.py new file mode 100644 index 00000000000000..40945385bee574 --- /dev/null +++ b/test_code/set_test/setcopy_test1.py @@ -0,0 +1,21 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +print(f"Initial Region: {r}") +class A: pass +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() + +r.arr1 = [r.a, r.b, r.c] +print(f"Region after setting arr1: {r}") +s1 = set(r.arr1) +print(f"Region after creating set s1: {r}") +s2 = s1.copy() +print(f"Region after copying s1 to s2: {r}") diff --git a/test_code/set_test/setpopdiscard_test1.py b/test_code/set_test/setpopdiscard_test1.py new file mode 100644 index 00000000000000..f13f5f7802bdfb --- /dev/null +++ b/test_code/set_test/setpopdiscard_test1.py @@ -0,0 +1,29 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +print(f"Initial Region: {r}") +class A: pass +freeze(A()) + +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() + +r.arr1 = [r.a, r.b, r.c] +print(f"Region after setting arr1: {r}") +s1 = set(r.arr1) +print(f"Region after creating set s1: {r}") + +# s1.pop() +# print(f"Region after popping from set s1: {r}") + +s1.discard(r.a) +print(f"Region after discarding r.a from set s1: {r}") +s1.discard(r.b) +print(f"Region after discarding r.b from set s1: {r}") +s1.discard(r.c) +print(f"Region after discarding r.c from set s1: {r}") From d7de1b4aaf0f32818583bc248ec79b68e5df285b Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Thu, 5 Mar 2026 14:20:14 +0100 Subject: [PATCH 14/37] add basic unit test for testing region of set data structure --- Lib/test/test_regions/test_set.py | 333 ++++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 Lib/test/test_regions/test_set.py diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py new file mode 100644 index 00000000000000..06c43659be194b --- /dev/null +++ b/Lib/test/test_regions/test_set.py @@ -0,0 +1,333 @@ +import unittest +from regions import Region, is_local +from immutable import freeze, isfrozen + + +class TestRegionSet(unittest.TestCase): + """Tests for basic set construction and LRC behavior with regions.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_set_from_region_array_increases_lrc(self): + """ + Creating a set from a region array should increase the LRC + for each element borrowed from the region. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + base_lrc = r._lrc + + s = set(r.arr) + + self.assertEqual(r._lrc, base_lrc + 2) + + def test_set_to_none_decreases_lrc(self): + """ + Setting the set to None should release the borrowed references + and bring LRC back to its pre-set level. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + base_lrc = r._lrc + + s = set(r.arr) + self.assertEqual(r._lrc, base_lrc + 2) + + s = None + self.assertEqual(r._lrc, base_lrc) + + def test_set_moved_into_region_adjusts_lrc(self): + """ + Moving a set into a region should transfer ownership of its elements, + reducing the LRC by the number of elements (now owned) minus the + external reference to the set itself. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.word3 = self.A() + r.word4 = self.A() + r.arr = [r.word, r.word2, r.word3, r.word4] + base_lrc = r._lrc + + s = set(r.arr) + # Set borrows all 4 elements + self.assertEqual(r._lrc, base_lrc + 4) + + # Moving into region: region owns the set, elements no longer borrowed + # but `s` still holds an external ref + r.set = s + self.assertEqual(r._lrc, base_lrc + 1) + + def test_set_from_set_in_region_increases_lrc(self): + """ + Creating a new set from a set that is already inside a region + should borrow all elements, increasing the LRC accordingly. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.word3 = self.A() + r.word4 = self.A() + r.arr = [r.word, r.word2, r.word3, r.word4] + s = set(r.arr) + r.set = s + base_lrc = r._lrc + + s2 = set(r.set) + self.assertEqual(r._lrc, base_lrc + 4) + + s2 = None + self.assertEqual(r._lrc, base_lrc) + + def test_set_from_dict_keys_with_object_keys(self): + """ + Creating a set from a dict that has object keys inside a region + should borrow those keys and increase the LRC. + """ + r = Region() + r.word = {self.A(): "value", self.A(): "value2"} + base_lrc = r._lrc + + s = set(r.word) + self.assertEqual(r._lrc, base_lrc + 2) + + def test_set_from_dict_with_frozen_string_keys(self): + """ + Creating a set from a dict with frozen string keys should + not affect the LRC since strings are frozen/immutable. + """ + r = Region() + r.word = {"key": "value", "key2": "value2"} + base_lrc = r._lrc + + s = set(r.word) + # Frozen string keys don't bump LRC + self.assertEqual(r._lrc, base_lrc) + + def test_set_move_into_region_fails_if_element_in_another_region(self): + """ + Moving a set into a region should fail if any element belongs + to a different region, and the state should remain consistent. + """ + r = Region() + r2 = Region() + r2.word4 = self.A() + + aa = self.A() + ab = self.A() + ac = self.A() + arr = [aa, ab, ac, r2.word4] + s = set(arr) + + with self.assertRaises(Exception): + r.set = s + + # Locals should still be local after the failed move + self.assertTrue(is_local(aa)) + self.assertTrue(is_local(ab)) + self.assertTrue(is_local(ac)) + + +class TestRegionSetDiscard(unittest.TestCase): + """Tests for set discard/pop operations and their effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_discard_decreases_lrc(self): + """ + Discarding an element from a set should release the borrowed + reference and decrease the LRC by 1 per discard. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + + original_lrc = r._lrc + s1 = set(r.arr1) + base_lrc = r._lrc + + s1.discard(r.a) + self.assertEqual(r._lrc, base_lrc - 1) + + s1.discard(r.b) + self.assertEqual(r._lrc, base_lrc - 2) + + s1.discard(r.c) + self.assertEqual(r._lrc, base_lrc - 3) + + self.assertEqual(r._lrc, original_lrc) + + def test_discard_nonexistent_element_does_not_change_lrc(self): + """ + Discarding an element that is not in the set should not + affect the LRC. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr1 = [r.a, r.b] + + s1 = set(r.arr1) + base_lrc = r._lrc + + s1.discard(r.a) # exists, LRC - 1 + self.assertEqual(r._lrc, base_lrc - 1) + + s1.discard(r.a) # already gone, should be no change + self.assertEqual(r._lrc, base_lrc - 1) + + +class TestRegionSetDifference(unittest.TestCase): + """Tests for set difference operations and their effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_set_difference_does_not_increase_lrc(self): + """ + Taking a set difference should produce a new local set. + The LRC of the source region should not increase since + the resulting set only contains elements not in the subtracted sets. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.a] + r.arr3 = [r.b] + + original_lrc = r._lrc + s1 = set(r.arr1) + self.assertEqual(r._lrc, original_lrc + 3) + s2 = set(r.arr2) + self.assertEqual(r._lrc, original_lrc + 3 + 1) + s3 = set(r.arr3) + self.assertEqual(r._lrc, original_lrc + 3 + 1 + 1) + base_lrc = r._lrc + + s4 = s1.difference(s2, s3) + self.assertEqual(r._lrc, base_lrc + 1) # s4 borrows r.c, but not r.a or r.b + + # s4 only contains r.c, so LRC should reflect that + self.assertTrue(is_local(s4)) + + def test_set_difference_result_releases_lrc_on_none(self): + """ + Setting the result of a difference to None should release + any borrowed references it holds. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.a] + r.arr3 = [r.b] + + s1 = set(r.arr1) + s2 = set(r.arr2) + s3 = set(r.arr3) + + s4 = s1.difference(s2, s3) + base_lrc = r._lrc + + s4 = None + self.assertLess(r._lrc, base_lrc) + + +class TestRegionSetSymmetricDifference(unittest.TestCase): + """Tests for symmetric difference (XOR) operations on sets.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_symmetric_difference_result_is_local(self): + """ + The result of a symmetric difference of two sets borrowing + from a region should be a local set. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + original_lrc = r._lrc + s1 = set(r.arr1) + self.assertEqual(r._lrc, original_lrc + 3) + s2 = set(r.arr2) + self.assertEqual(r._lrc, original_lrc + 3 + 3) + base_lrc = r._lrc + + result = s1.symmetric_difference(s2) + self.assertEqual(r._lrc, base_lrc + 2) + self.assertTrue(is_local(result)) + + def test_symmetric_difference_lrc_released_on_none(self): + """ + Releasing the result of symmetric_difference should bring + the LRC back down by the number of unique elements it held. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + s1 = set(r.arr1) + s2 = set(r.arr2) + + result = s1.symmetric_difference(s2) + base_lrc = r._lrc + + result = None + # r.a and r.f are the unique elements, so LRC should drop by 2 + self.assertEqual(r._lrc, base_lrc - 2) + + def test_symmetric_difference_operator_matches_method(self): + """ + The `^` operator should behave identically to symmetric_difference(). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + result_method = s1.symmetric_difference(s2) + result_operator = s1 ^ s2 + self.assertEqual(r._lrc, base_lrc + 2 + 2) # both should borrow the same unique elements, and there are two result, +2 from result_method and +2 from result_operator + + self.assertEqual(result_method, result_operator) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 8b9650633fccb9c10f1fc1e6980688260a564e28 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Fri, 6 Mar 2026 11:52:24 +0100 Subject: [PATCH 15/37] update set migration with unit-test --- Lib/test/test_regions/test_set.py | 772 +++++++++++++++++++++++++++++- Objects/setobject.c | 13 +- 2 files changed, 764 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index 06c43659be194b..9935cc888afd42 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -146,30 +146,26 @@ class A: pass def test_discard_decreases_lrc(self): """ - Discarding an element from a set should release the borrowed - reference and decrease the LRC by 1 per discard. + Discarding an element from a set should release the subregion's parent. """ - r = Region() - r.a = self.A() - r.b = self.A() - r.c = self.A() - r.arr1 = [r.a, r.b, r.c] - - original_lrc = r._lrc - s1 = set(r.arr1) - base_lrc = r._lrc - - s1.discard(r.a) - self.assertEqual(r._lrc, base_lrc - 1) + r1 = Region() + r2 = Region() + r3 = Region() + r4 = Region() - s1.discard(r.b) - self.assertEqual(r._lrc, base_lrc - 2) + s1 = set([r1, r2, r3]) + r4.s = s1 + self.assertEqual(r1.parent, r4) - s1.discard(r.c) - self.assertEqual(r._lrc, base_lrc - 3) + s1.discard(r1) + self.assertIsNone(r1.parent) - self.assertEqual(r._lrc, original_lrc) + s1.discard(r2) + self.assertIsNone(r2.parent) + s1.discard(r3) + self.assertIsNone(r3.parent) + def test_discard_nonexistent_element_does_not_change_lrc(self): """ Discarding an element that is not in the set should not @@ -328,6 +324,744 @@ def test_symmetric_difference_operator_matches_method(self): self.assertEqual(result_method, result_operator) +class TestRegionFrozenSet(unittest.TestCase): + """Tests for frozenset construction, ownership transfer, and copy behavior.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_frozenset_from_region_array_increases_lrc(self): + """ + Creating a frozenset from a region array should borrow + all elements and increase the LRC accordingly. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + base_lrc = r._lrc + + s1 = frozenset(r.arr1) + self.assertEqual(r._lrc, base_lrc + 3) + + def test_frozenset_moved_into_region_adjusts_lrc(self): + """ + Moving a frozenset into a region transfers ownership of its + elements, reducing the borrowed references accordingly. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + + s1 = frozenset(r.arr1) + r.set1 = s1 + base_lrc = r._lrc + + # s1 still holds an external ref but elements are now owned + self.assertEqual(r._lrc, base_lrc) + + def test_frozenset_copy_is_same_object(self): + """ + Copying a frozenset that is already inside a region should + return the same object (CPython optimizes frozenset copies), + not a new independent frozenset. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + + s1 = frozenset(r.arr1) + r.set1 = s1 + + s2 = s1.copy() + self.assertIs(s2, r.set1) + + def test_frozenset_copy_does_not_change_lrc(self): + """ + Since frozenset.copy() returns the same object, the LRC + should increase by exactly 1 for the new reference. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + + s1 = frozenset(r.arr1) + r.set1 = s1 + base_lrc = r._lrc + + s2 = s1.copy() + self.assertEqual(r._lrc, base_lrc + 1) + + s2 = None + self.assertEqual(r._lrc, base_lrc) + + def test_frozenset_from_another_frozenset(self): + """ + Creating a frozenset from another frozenset should return the same object, + and not increase the LRC since it's not borrowing new references. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + + s1 = frozenset(r.arr1) + r.set1 = s1 + base_lrc = r._lrc + + s2 = frozenset(s1) + self.assertIs(s2, s1) + self.assertEqual(r._lrc, base_lrc + 1) # only the original reference to s1 increases LRC + + +class TestRegionSetCopy(unittest.TestCase): + """Tests for set copy behavior and its effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_copy_increases_lrc(self): + """ + Copying a set that borrows from a region should increase + the LRC since the copy also holds references to the same elements. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + + s1 = set(r.arr1) + base_lrc = r._lrc + + s2 = s1.copy() + self.assertEqual(r._lrc, base_lrc + 3) + + def test_copy_released_decreases_lrc(self): + """ + Releasing the copied set should bring LRC back down + to the level before the copy was made. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + + s1 = set(r.arr1) + base_lrc = r._lrc + + s2 = s1.copy() + self.assertEqual(r._lrc, base_lrc + 3) + + s2 = None + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionSetIntersection(unittest.TestCase): + """Tests for set intersection operations and their effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_intersection_result_is_local(self): + """ + The result of an intersection of two sets borrowing from + a region should be a local set. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + s1 = set(r.arr1) + s2 = set(r.arr2) + + result = s1.intersection(s2) + self.assertTrue(is_local(result)) + + def test_intersection_lrc_reflects_common_elements(self): + """ + The intersection result only holds references to shared elements, + so the LRC increase should reflect only those elements. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.b, r.c, r.f] # {b, c, f} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + result = s1.intersection(s2) # {b, c} + self.assertEqual(r._lrc, base_lrc + 2) + + result = None + self.assertEqual(r._lrc, base_lrc) + + def test_intersection_operator_matches_method(self): + """ + The `&` operator should behave identically to intersection(). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + result_method = s1.intersection(s2) + result_operator = s1 & s2 + self.assertEqual(result_method, result_operator) + self.assertEqual(r._lrc, base_lrc + 2 + 2) # both should borrow the same common elements, and there are two common elements, +2 from result_method and +2 from result_operator + + def test_intersection_multiple_sets(self): + """ + Intersection across three sets should only retain elements + common to all three. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + r.arr3 = [r.b, r.d, r.e] + + s1 = set(r.arr1) + s2 = set(r.arr2) + s3 = set(r.arr3) + base_lrc = r._lrc + + result = s1.intersection(s2, s3) # only {b} + self.assertEqual(r._lrc, base_lrc + 1) + + result = None + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionSetIntersectionUpdate(unittest.TestCase): + """Tests for in-place intersection (intersection_update / &=) and LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_intersection_update_removes_non_common_refs(self): + """ + intersection_update should release references to elements removed + from s1 and retain only those in the intersection. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.b, r.c, r.f] # {b, c, f} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + s1.intersection_update(s2) # s1 becomes {b, c}, drops ref to a + # s1 now holds 2 refs (b, c), s2 holds 3 refs (b, c, f) + self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for dropping a b c, +2 for retaining b and c + + def test_intersection_update_operator_matches_method(self): + """ + The `&=` operator should behave identically to intersection_update(). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + s1_method = set(r.arr1) + s1_operator = set(r.arr1) + s2 = set(r.arr2) + + s1_method.intersection_update(s2) + s1_operator &= s2 + self.assertEqual(s1_method, s1_operator) + + +class TestRegionSetUnion(unittest.TestCase): + """Tests for set union operations and their effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_union_result_is_local(self): + """ + The result of a union of two sets borrowing from a region + should be a local set. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + s1 = set(r.arr1) + s2 = set(r.arr2) + + result = s1 | s2 + self.assertTrue(is_local(result)) + + def test_union_lrc_reflects_all_unique_elements(self): + """ + The union result holds references to all unique elements across + both sets, so the LRC should increase by the count of unique elements. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.b, r.c, r.f] # {b, c, f} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + result = s1 | s2 # {a, b, c, f} + self.assertEqual(r._lrc, base_lrc + 4) + + result = None + self.assertEqual(r._lrc, base_lrc) + + def test_union_operator_matches_method(self): + """ + The `|` operator should behave identically to union(). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + s1 = set(r.arr1) + s2 = set(r.arr2) + + self.assertEqual(s1 | s2, s1.union(s2)) + + +class TestRegionSetUnionUpdate(unittest.TestCase): + """Tests for in-place union (|=) and LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_union_update_adds_new_refs(self): + """ + |= should add references to new elements from s2 that + weren't already in s1, increasing the LRC accordingly. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.b, r.c, r.f] # {b, c, f} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + s1 |= s2 # s1 becomes {a, b, c, f}, gains ref to f + self.assertEqual(r._lrc, base_lrc - 3 + 4) # -3 for original a b c, +4 for new a b c f (but a b c are still borrowed, so net +1 for f) + + def test_union_update_released_decreases_lrc(self): + """ + Releasing s1 after |= should drop all the references it holds. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + s2 = set(r.arr2) + base_lrc = r._lrc + + s1 = set(r.arr1) + s1 |= s2 + s1 = None + # All s1 refs released, only s2's refs remain + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionSetDifferenceUpdate(unittest.TestCase): + """Tests for in-place difference (-=) and LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_difference_update_removes_refs(self): + """ + -= should release references to elements removed from s1, + decreasing the LRC by the number of elements subtracted. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.a] # {a} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + s1 -= s2 # s1 becomes {b, c}, drops ref to a + self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for original a b c, +2 for remaining b and c + + def test_difference_update_result_released(self): + """ + Releasing s1 after -= should drop all remaining references it holds. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.a] + + s2 = set(r.arr2) + base_lrc = r._lrc + + s1 = set(r.arr1) + s1 -= s2 + s1 = None + # Only s2's ref to a remains + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionSetSymmetricDifferenceUpdate(unittest.TestCase): + """Tests for in-place symmetric difference (^= / symmetric_difference_update) and LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_symmetric_difference_update_adjusts_lrc(self): + """ + symmetric_difference_update should release refs to common elements + and add refs to new unique elements from s2. + arr1 = {a, b, c, f}, arr2 = {a, b, f} → result = {c} + Drops a, b, f (3 refs), keeps c (1 ref). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c, r.f] # {a, b, c, f} + r.arr2 = [r.a, r.b, r.f] # {a, b, f} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + s1.symmetric_difference_update(s2) # s1 becomes {c} + self.assertEqual(r._lrc, base_lrc - 4 + 1) # -4 for original a b c f, +1 for remaining c + + def test_symmetric_difference_update_operator_matches_method(self): + """ + The `^=` operator should behave identically to symmetric_difference_update(). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c, r.f] + r.arr2 = [r.a, r.b, r.f] + + s1_method = set(r.arr1) + s1_operator = set(r.arr1) + s2 = set(r.arr2) + + s1_method.symmetric_difference_update(s2) + s1_operator ^= s2 + self.assertEqual(s1_method, s1_operator) + + +class TestRegionSetSubsetSuperset(unittest.TestCase): + """Tests for issubset / issuperset checks — these are read-only so LRC should not change.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_issubset_true(self): + """ + s1 = {a} is a subset of s2 = {a, b, c}. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a] + r.arr2 = [r.a, r.b, r.c] + + s1 = set(r.arr1) + s2 = set(r.arr2) + + self.assertTrue(s1.issubset(s2)) + + def test_issubset_false(self): + """ + s1 = {a, b, c} is not a subset of s2 = {a}. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.a] + + s1 = set(r.arr1) + s2 = set(r.arr2) + + self.assertFalse(s1.issubset(s2)) + + def test_issubset_operator_matches_method(self): + """ + The `<=` operator should behave identically to issubset(). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a] + r.arr2 = [r.a, r.b, r.c] + + s1 = set(r.arr1) + s2 = set(r.arr2) + + self.assertEqual(s1.issubset(s2), s1 <= s2) + + def test_issubset_does_not_change_lrc(self): + """ + issubset is a read-only operation and should not affect the LRC. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a] + r.arr2 = [r.a, r.b, r.c] + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + _ = s1.issubset(s2) + self.assertEqual(r._lrc, base_lrc) + + def test_issuperset_true(self): + """ + s2 = {a, b, c} is a superset of s1 = {a}. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a] + r.arr2 = [r.a, r.b, r.c] + + s1 = set(r.arr1) + s2 = set(r.arr2) + + self.assertTrue(s2.issuperset(s1)) + + def test_issuperset_false(self): + """ + s1 = {a} is not a superset of s2 = {a, b, c}. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a] + r.arr2 = [r.a, r.b, r.c] + + s1 = set(r.arr1) + s2 = set(r.arr2) + + self.assertFalse(s1.issuperset(s2)) + + def test_issuperset_strict_operator(self): + """ + s2 > s1 (strict superset) should be False when s1 == s2. + """ + r = Region() + r.a = self.A() + r.arr1 = [r.a] + + s1 = set(r.arr1) + s2 = set(r.arr1) + + self.assertFalse(s2 > s1) + + def test_issuperset_does_not_change_lrc(self): + """ + issuperset is a read-only operation and should not affect the LRC. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a] + r.arr2 = [r.a, r.b, r.c] + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + _ = s2.issuperset(s1) + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionSetIterator(unittest.TestCase): + """Tests for set iterator behavior and its effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_iter_creation_does_not_change_lrc(self): + """ + Creating an iterator over a set should not by itself + change the LRC of the region. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + + s = set(r.arr) + base_lrc = r._lrc + + it = iter(s) + self.assertEqual(r._lrc, base_lrc) + + def test_next_on_iter_increases_lrc(self): + """ + Calling next() on the iterator yields a borrowed reference, + increasing the LRC by 1. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.word3 = self.A() + r.word4 = self.A() + r.arr = [r.word, r.word2, r.word3, r.word4] + + s = set(r.arr) + it = iter(s) + base_lrc = r._lrc + + a1 = next(it) + self.assertEqual(r._lrc, base_lrc + 1) + + a2 = next(it) + self.assertEqual(r._lrc, base_lrc + 2) + + r.a3 = next(it) + self.assertEqual(r._lrc, base_lrc + 2) + + a4 = next(it) + self.assertEqual(r._lrc, base_lrc + 3) + + def test_iter_to_none_releases_lrc(self): + """ + Setting the iterator to None should release the iterator's + reference and bring LRC back to the pre-iterator level. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + + s = set(r.arr) + it = iter(s) + base_lrc = r._lrc + + it = None + self.assertEqual(r._lrc, base_lrc) + + def test_next_into_region_transfers_ownership(self): + """ + Assigning next() result into a region should transfer ownership + rather than keeping it as a borrowed external reference. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.word3 = self.A() + r.arr = [r.word, r.word2, r.word3] + + s = set(r.arr) + it = iter(s) + base_lrc = r._lrc + + a1 = next(it) # external local ref, LRC + 1 + r.a3 = next(it) # moved into region, no external borrow + self.assertEqual(r._lrc, base_lrc + 1) + if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/Objects/setobject.c b/Objects/setobject.c index dfc39f82bc6558..15116182564b79 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -811,10 +811,14 @@ set_pop_impl(PySetObject *so) entry = so->table; } key = entry->key; + if(PyRegion_AddLocalRef(key)) { // Bc key has to be returned, it is still there, maybe in or not in the region. (The reference the function returns) + return NULL; + } entry->key = dummy; entry->hash = -1; FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used - 1); so->finger = entry - so->table + 1; /* next place to start */ + PyRegion_RemoveRef(so, key); return key; } @@ -1339,7 +1343,7 @@ set_new(PyTypeObject *type, PyObject *args, PyObject *kwds) result to be swapped into one of the original inputs). */ -static void +static int set_swap_bodies(PySetObject *a, PySetObject *b) { Py_ssize_t t; @@ -1347,6 +1351,7 @@ set_swap_bodies(PySetObject *a, PySetObject *b) setentry tab[PySet_MINSIZE]; Py_hash_t h; + // if a and b are in the same region, no problem. use this t = a->fill; a->fill = b->fill; b->fill = t; t = a->used; FT_ATOMIC_STORE_SSIZE_RELAXED(a->used, b->used); @@ -1376,6 +1381,7 @@ set_swap_bodies(PySetObject *a, PySetObject *b) FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, -1); FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, -1); } + // else, use 4 barriers before swapping } /*[clinic input] @@ -1631,6 +1637,9 @@ set_intersection_multi_impl(PySetObject *so, PyObject * const *others, return set_copy((PyObject *)so, NULL); } + if(PyRegion_AddLocalRef(so)) { + return NULL; + } PyObject *result = Py_NewRef(so); for (i = 0; i < others_length; i++) { PyObject *other = others[i]; @@ -1838,7 +1847,7 @@ set_difference_update_internal(PySetObject *so, PyObject *other) return -1; } Py_INCREF(key); - // DONE Migration: Maybe? + // DONE Migration if (set_discard_entry(so, key, entry->hash) < 0) { PyRegion_RemoveLocalRef(other); PyRegion_RemoveLocalRef(key); From f6dd87cf0c4244082aec5979f3e91f06dd42501d Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Sun, 8 Mar 2026 15:02:10 +0100 Subject: [PATCH 16/37] update unit test --- Lib/test/test_regions/test_set.py | 50 +++++++++++++++++ test_code/set_test/setand_test1.py | 84 +++++++++++++++++++---------- test_code/set_test/setiand_test1.py | 46 +++++++++++++--- 3 files changed, 145 insertions(+), 35 deletions(-) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index 9935cc888afd42..91c0f79b3d7745 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -568,6 +568,34 @@ def test_intersection_multiple_sets(self): result = None self.assertEqual(r._lrc, base_lrc) + def test_intersection_multiple_sets_2(self): + """ + Intersection across three sets should only retain elements + common to all three. Some sets are now in the region, and some are in local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + r.arr3 = [r.b, r.e, r.f] + + original_lrc = r._lrc + r.s1 = set(r.arr1) + self.assertEqual(r._lrc, original_lrc) + s2 = set(r.arr2) + self.assertEqual(r._lrc, original_lrc + 3) + r.s3 = set(r.arr3) + self.assertEqual(r._lrc, original_lrc + 3) + base_lrc = r._lrc + + result = r.s1.intersection(s2, r.s3) + self.assertEqual(r._lrc, base_lrc + 1) + class TestRegionSetIntersectionUpdate(unittest.TestCase): """Tests for in-place intersection (intersection_update / &=) and LRC.""" @@ -617,6 +645,28 @@ def test_intersection_update_operator_matches_method(self): s1_method.intersection_update(s2) s1_operator &= s2 self.assertEqual(s1_method, s1_operator) + + def test_intersection_update_swap_bodies_different_region(self): + """ + the first set is in the region, but the second set (tmp) is local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + original_lrc = r._lrc + r.arr1 = [r.a, r.b, r.c] + arr2 = [r.b, r.c, r.f] + self.assertEqual(r._lrc, original_lrc + 3) + + r.s1 = set(r.arr1) + s2 = set(arr2) + self.assertEqual(r._lrc, original_lrc + 3 + 3) + base_lrc = r._lrc + + r.s1.intersection_update(s2) + self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for dropping a b c, +2 for retaining b and c class TestRegionSetUnion(unittest.TestCase): diff --git a/test_code/set_test/setand_test1.py b/test_code/set_test/setand_test1.py index 824372d6dada03..e8b4eb984dba0b 100644 --- a/test_code/set_test/setand_test1.py +++ b/test_code/set_test/setand_test1.py @@ -1,45 +1,75 @@ from regions import Region, is_local from immutable import freeze, register_freezable -r = Region() +r1 = Region() r2 = Region() -print(f"Initial Region: {r}") +r3 = Region() +r4 = Region() + class A: pass; freeze(A()) -r.a = A() -r.b = A() -r.c = A() -r.d = A() -r.e = A() -r.f = A() -r.arr1 = [r.a, r.b, r.c] -r.arr2 = [r.b, r.c, r.f] -r.arr3 = [r.b, r.d, r.e] - -s1 = set(r.arr1) -print(f"Region after creating set1: {r}") -print(f"{s1}") -s2 = set(r.arr2) -print(f"Region after creating set2: {r}") -print(f"{s2}") -input("Press Enter to create set intersection...") -# result = s1 & s2 -result = s1.intersection(s2) -print(f"Region after creating set intersection: {r}") -print(f"Intersection result: {result}") +r1.a = A() +r1.b = A() +r1.c = A() +r1.d = A() +r1.e = A() +r1.f = A() +r1.arr1 = [r1.a, r1.b, r1.c] +r1.arr2 = [r1.b, r1.c, r1.f] +r1.arr3 = [r1.b, r1.d, r1.e] +# print(f"Initial Region: {r}") # s1 = set(r.arr1) # print(f"Region after creating set1: {r}") # print(f"{s1}") # s2 = set(r.arr2) # print(f"Region after creating set2: {r}") # print(f"{s2}") -# s3 = set(r.arr3) -# print(f"Region after creating set3: {r}") +# input("Press Enter to create set intersection...") +# # result = s1 & s2 +# result = s1.intersection(s2) +# print(f"Region after creating set intersection: {r}") +# print(f"Intersection result: {result}") + +# print(f"Initial Region: {r1}") +# s1 = set(r1.arr1) +# print(f"Region after creating set1: {r1}") +# print(f"{s1}") +# s2 = set(r1.arr2) +# print(f"Region after creating set2: {r1}") +# print(f"{s2}") +# s3 = set(r1.arr3) +# print(f"Region after creating set3: {r1}") # print(f"{s3}") # input("Press Enter to create set intersection...") # result = s1.intersection(s2, s3) -# print(f"Region after creating set intersection: {r}") -# print(f"Intersection result: {result}") \ No newline at end of file +# print(f"Region after creating set intersection: {r1}") +# print(f"Intersection result: {result}") + +print(f"Initial Region: {r1}") +r1.s1 = set(r1.arr1) +print(f"Region after creating set1: {r1}") +print(f"{r1.s1}") +s2 = set(r1.arr2) +print(f"Region after creating set2: {r1}") +print(f"{s2}") +r1.s3 = set(r1.arr3) +print(f"Region after creating set3: {r1}") +print(f"{r1.s3}") +input("Press Enter to create set intersection...") +result = r1.s1.intersection(s2, r1.s3) +print(f"Region after creating set intersection: {r1}") +print(f"Intersection result: {result}") + +# print(f"Initial Region: {r1}") +# print(f"Region r2: {r2}") +# print(f"Region r3: {r3}") +# print(f"Region r4: {r4}\n") +# r1.set1 = set([r2, r3]) +# print(f"Region after creating set1: {r1}") +# print(f"Region r2: {r2}") +# print(f"Region r3: {r3}") +# print(f"Region r4: {r4}\n") +# r1.set2 = set([r2, r4]) \ No newline at end of file diff --git a/test_code/set_test/setiand_test1.py b/test_code/set_test/setiand_test1.py index cedc9f8b7eacae..1ca794b6f0beb6 100644 --- a/test_code/set_test/setiand_test1.py +++ b/test_code/set_test/setiand_test1.py @@ -14,16 +14,46 @@ class A: pass; r.d = A() r.e = A() r.f = A() + +# r.arr1 = [r.a, r.b, r.c] +# r.arr2 = [r.b, r.c, r.f] +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") # +3 +# print(f"{s1}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") # +3 +# print(f"{s2}") +# input("Press Enter to create set intersection...") +# # s1 &= s2 +# s1.intersection_update(s2) +# print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1, then +2: add the reference from s1 to the result +# print(f"Intersection result: {s1}") + +# r.arr1 = [r.a, r.b, r.c] +# r.arr2 = [r.f] +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") # +3 +# print(f"{s1}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") # +3 +# print(f"{s2}") +# input("Press Enter to create set intersection...") +# s1 &= s2 +# # s1.intersection_update(s2) +# print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1 +# print(f"Intersection result: {s1}") + r.arr1 = [r.a, r.b, r.c] -r.arr2 = [r.b, r.c, r.f] -s1 = set(r.arr1) -print(f"Region after creating set1: {r}") # +3 -print(f"{s1}") -s2 = set(r.arr2) +arr2 = [r.a, r.b, r.f] +print(f"Region after creating arr1 and arr2: {r}") # +3 +r.s1 = set(r.arr1) +print(f"Region after creating set1: {r}") # +0 +print(f"{r.s1}") +s2 = set(arr2) print(f"Region after creating set2: {r}") # +3 print(f"{s2}") input("Press Enter to create set intersection...") -# s1 &= s2 -s1.intersection_update(s2) +# r.s1 &= s2 # Wrong +r.s1.intersection_update(s2) print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1, then +2: add the reference from s1 to the result -print(f"Intersection result: {s1}") \ No newline at end of file +print(f"Intersection result: {r.s1}") \ No newline at end of file From 15f2e5dfb35a894f813351e7e8e44d0780c6a231 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Sun, 8 Mar 2026 15:11:40 +0100 Subject: [PATCH 17/37] update set unit test for intersection and intersection_update --- Lib/test/test_regions/test_set.py | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index 91c0f79b3d7745..a33b3084e40482 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -625,6 +625,27 @@ def test_intersection_update_removes_non_common_refs(self): s1.intersection_update(s2) # s1 becomes {b, c}, drops ref to a # s1 now holds 2 refs (b, c), s2 holds 3 refs (b, c, f) self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for dropping a b c, +2 for retaining b and c + + def test_intersection_update_removes_non_common_refs_2(self): + """ + intersection_update should release references to elements removed + from s1 and retain only those in the intersection. Using &= instead. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.b, r.c, r.f] # {b, c, f} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + s1 &= s2 # s1 becomes {b, c}, drops ref to a + # s1 now holds 2 refs (b, c), s2 holds 3 refs (b, c, f) + self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for dropping a b c, +2 for retaining b and c def test_intersection_update_operator_matches_method(self): """ @@ -667,6 +688,32 @@ def test_intersection_update_swap_bodies_different_region(self): r.s1.intersection_update(s2) self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for dropping a b c, +2 for retaining b and c + + def test_intersection_update_multi_swap_bodies_different_region(self): + """ + the first and thirdset is in the region, but the second set (tmp) is local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + original_lrc = r._lrc + r.arr1 = [r.a, r.b, r.c] + arr2 = [r.b, r.c, r.f] + r.arr3 = [r.b, r.d, r.e] + self.assertEqual(r._lrc, original_lrc + 3) + + r.s1 = set(r.arr1) + s2 = set(arr2) + r.s3 = set(r.arr3) + self.assertEqual(r._lrc, original_lrc + 3 + 3) + base_lrc = r._lrc + + r.s1.intersection_update(s2, r.s3) + self.assertEqual(r._lrc, base_lrc - 3 + 1) # -3 for dropping a b c, +1 for retaining b and c class TestRegionSetUnion(unittest.TestCase): From 94e360bec45787d4291b2fbb3fcea28472595e73 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Sun, 8 Mar 2026 15:56:09 +0100 Subject: [PATCH 18/37] update set unit test in intersectupdate --- Lib/test/test_regions/test_set.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index a33b3084e40482..dd2c0b5604afe0 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -669,7 +669,7 @@ def test_intersection_update_operator_matches_method(self): def test_intersection_update_swap_bodies_different_region(self): """ - the first set is in the region, but the second set (tmp) is local. + the first set is in the local, but the second set is in the region. """ r = Region() r.a = self.A() @@ -677,21 +677,22 @@ def test_intersection_update_swap_bodies_different_region(self): r.c = self.A() r.f = self.A() original_lrc = r._lrc - r.arr1 = [r.a, r.b, r.c] - arr2 = [r.b, r.c, r.f] + arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] self.assertEqual(r._lrc, original_lrc + 3) - r.s1 = set(r.arr1) - s2 = set(arr2) + s1 = set(arr1) + r.s2 = set(r.arr2) self.assertEqual(r._lrc, original_lrc + 3 + 3) base_lrc = r._lrc - r.s1.intersection_update(s2) + s1.intersection_update(r.s2) self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for dropping a b c, +2 for retaining b and c + @unittest.expectedFailure def test_intersection_update_multi_swap_bodies_different_region(self): """ - the first and thirdset is in the region, but the second set (tmp) is local. + the first and third set is in the region, but the second set is local. """ r = Region() r.a = self.A() @@ -713,7 +714,7 @@ def test_intersection_update_multi_swap_bodies_different_region(self): base_lrc = r._lrc r.s1.intersection_update(s2, r.s3) - self.assertEqual(r._lrc, base_lrc - 3 + 1) # -3 for dropping a b c, +1 for retaining b and c + self.assertEqual(r._lrc, base_lrc) # should not change LRC since s1 is in the region. LRC should not be updated class TestRegionSetUnion(unittest.TestCase): From df8d051fcce24f065f9a17803072c3e4204eb8de Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Tue, 10 Mar 2026 10:22:00 +0100 Subject: [PATCH 19/37] update set unit test and testcase --- Lib/test/test_regions/test_set.py | 49 ++++++++++++++++++++++++++++- test_code/set_test/setiand_test1.py | 41 +++++++++++++++++++++--- test_code/set_test/setixor_test1.py | 21 ++++++++++--- 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index dd2c0b5604afe0..4d8b7b8bd180c5 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -568,7 +568,31 @@ def test_intersection_multiple_sets(self): result = None self.assertEqual(r._lrc, base_lrc) - def test_intersection_multiple_sets_2(self): + def test_intersection_multiple_sets_2_1(self): + """ + Intersection across two sets should only retain elements + common to all two. The first set is in the region, and the second set + is in local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + + original_lrc = r._lrc + r.s1 = set(r.arr1) + self.assertEqual(r._lrc, original_lrc) + s2 = set(r.arr2) + self.assertEqual(r._lrc, original_lrc + 3) + base_lrc = r._lrc + + result = r.s1.intersection(s2) + self.assertEqual(r._lrc, base_lrc + 2) + + def test_intersection_multiple_sets_2_2(self): """ Intersection across three sets should only retain elements common to all three. Some sets are now in the region, and some are in local. @@ -688,6 +712,29 @@ def test_intersection_update_swap_bodies_different_region(self): s1.intersection_update(r.s2) self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for dropping a b c, +2 for retaining b and c + + @unittest.expectedFailure + def test_intersection_update_swap_bodies_different_region_2(self): + """ + the first set is in the region, but the second set is in the local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + original_lrc = r._lrc + r.arr1 = [r.a, r.b, r.c] + arr2 = [r.b, r.c, r.f] + self.assertEqual(r._lrc, original_lrc + 3) + + r.s1 = set(r.arr1) + s2 = set(arr2) + self.assertEqual(r._lrc, original_lrc + 3 + 3) + base_lrc = r._lrc + + r.s1.intersection_update(s2) + self.assertEqual(r._lrc, base_lrc) # should not change since s1 is in the region. LRC should not be updated since s1 is in the region. @unittest.expectedFailure def test_intersection_update_multi_swap_bodies_different_region(self): diff --git a/test_code/set_test/setiand_test1.py b/test_code/set_test/setiand_test1.py index 1ca794b6f0beb6..a691e2642bd7d2 100644 --- a/test_code/set_test/setiand_test1.py +++ b/test_code/set_test/setiand_test1.py @@ -44,7 +44,7 @@ class A: pass; # print(f"Intersection result: {s1}") r.arr1 = [r.a, r.b, r.c] -arr2 = [r.a, r.b, r.f] +arr2 = [r.a, r.f] print(f"Region after creating arr1 and arr2: {r}") # +3 r.s1 = set(r.arr1) print(f"Region after creating set1: {r}") # +0 @@ -53,7 +53,40 @@ class A: pass; print(f"Region after creating set2: {r}") # +3 print(f"{s2}") input("Press Enter to create set intersection...") -# r.s1 &= s2 # Wrong +# r.s1 &= s2 r.s1.intersection_update(s2) -print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1, then +2: add the reference from s1 to the result -print(f"Intersection result: {r.s1}") \ No newline at end of file +print(f"Region after creating set intersection: {r}") +print(f"Intersection result: {r.s1}") + +# r.arr1 = [r.a, r.b, r.c] +# arr2 = [r.a, r.b, r.f] +# r.arr3 = [r.a] +# print(f"Region after creating arr1 and arr2: {r}") # +3 +# r.s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") # +0 +# print(f"{r.s1}") +# s2 = set(arr2) +# print(f"Region after creating set2: {r}") # +3 +# print(f"{s2}") +# r.s3 = set(r.arr3) +# print(f"Region after creating set3: {r}") # +0 +# print(f"{r.s3}") +# input("Press Enter to create set intersection...") +# r.s1.intersection_update(s2, r.s3) +# print(f"Region after creating set intersection: {r}") # Wrong: LRC should not be changed since s1 is in the region +# print(f"Intersection result: {r.s1}") + +# arr1 = [r.a, r.b, r.c] +# r.arr2 = [r.a, r.b, r.f] +# print(f"Region after creating arr1 and arr2: {r}") # +3 +# s1 = set(arr1) +# print(f"Region after creating set1: {r}") # +3 +# print(f"{s1}") +# r.s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") # +0 +# print(f"{r.s2}") +# input("Press Enter to create set intersection...") +# s1 &= r.s2 +# # s1.intersection_update(r.s2) +# print(f"Region after creating set intersection: {r}") +# print(f"Intersection result: {s1}") \ No newline at end of file diff --git a/test_code/set_test/setixor_test1.py b/test_code/set_test/setixor_test1.py index f05058ee659aeb..7ac5bd46006784 100644 --- a/test_code/set_test/setixor_test1.py +++ b/test_code/set_test/setixor_test1.py @@ -16,17 +16,30 @@ class A: pass; r.f = A() r.arr1 = [r.a, r.b, r.c, r.f] r.arr2 = [r.a, r.b, r.f] -s1 = set(r.arr1) + +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") # +4 +# print(f"{s1}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") # +3 +# print(f"{s2}") +# input("Press Enter to create set xor...") +# # s1 ^= s2 +# s1.symmetric_difference_update(s2) # -4 for derefs r.a, r.b, r.c, r.f, then +1 for r.c +# print(f"Region after creating set xor: {r}") +# print(f"Xor result: {s1}") + +r.s1 = set(r.arr1) print(f"Region after creating set1: {r}") # +4 -print(f"{s1}") +print(f"{r.s1}") s2 = set(r.arr2) print(f"Region after creating set2: {r}") # +3 print(f"{s2}") input("Press Enter to create set xor...") # s1 ^= s2 -s1.symmetric_difference_update(s2) # -4 for derefs r.a, r.b, r.c, r.f, then +1 for r.c +r.s1.symmetric_difference_update(s2) # -4 for derefs r.a, r.b, r.c, r.f, then +1 for r.c print(f"Region after creating set xor: {r}") -print(f"Xor result: {s1}") +print(f"Xor result: {r.s1}") # r.a = A() # r.b = A() From bd635e9013783a68e61717096af7934b7ca7f09c Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Tue, 10 Mar 2026 15:01:36 +0100 Subject: [PATCH 20/37] update test cases and unit test --- Lib/test/test_regions/test_set.py | 192 +++++++++++++++++++++++++++- test_code/set_test/setiand_test1.py | 12 +- test_code/set_test/setisub_test1.py | 22 +++- test_code/set_test/setixor_test1.py | 4 +- 4 files changed, 214 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index 4d8b7b8bd180c5..1124342a2417a3 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -713,10 +713,9 @@ def test_intersection_update_swap_bodies_different_region(self): s1.intersection_update(r.s2) self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for dropping a b c, +2 for retaining b and c - @unittest.expectedFailure - def test_intersection_update_swap_bodies_different_region_2(self): + def test_intersection_update_swap_bodies_different_region_2_intersection_update(self): """ - the first set is in the region, but the second set is in the local. + the first set is in the region, but the second set is in the local. Using intersection_update(). """ r = Region() r.a = self.A() @@ -735,8 +734,30 @@ def test_intersection_update_swap_bodies_different_region_2(self): r.s1.intersection_update(s2) self.assertEqual(r._lrc, base_lrc) # should not change since s1 is in the region. LRC should not be updated since s1 is in the region. - + @unittest.expectedFailure + def test_intersection_update_swap_bodies_different_region_2_iand(self): + """ + the first set is in the region, but the second set is in the local. Using &=. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + original_lrc = r._lrc + r.arr1 = [r.a, r.b, r.c] + arr2 = [r.b, r.c, r.f] + self.assertEqual(r._lrc, original_lrc + 3) + + r.s1 = set(r.arr1) + s2 = set(arr2) + self.assertEqual(r._lrc, original_lrc + 3 + 3) + base_lrc = r._lrc + + r.s1 &= s2 + self.assertEqual(r._lrc, base_lrc) # should not change since s1 is in the region. LRC should not be updated since s1 is in the region. + def test_intersection_update_multi_swap_bodies_different_region(self): """ the first and third set is in the region, but the second set is local. @@ -844,6 +865,7 @@ def test_union_update_adds_new_refs(self): """ |= should add references to new elements from s2 that weren't already in s1, increasing the LRC accordingly. + Using |=. """ r = Region() r.a = self.A() @@ -859,6 +881,71 @@ def test_union_update_adds_new_refs(self): s1 |= s2 # s1 becomes {a, b, c, f}, gains ref to f self.assertEqual(r._lrc, base_lrc - 3 + 4) # -3 for original a b c, +4 for new a b c f (but a b c are still borrowed, so net +1 for f) + + def test_union_update_adds_new_refs_update(self): + """ + |= should add references to new elements from s2 that + weren't already in s1, increasing the LRC accordingly. + Using update instead of |=. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.b, r.c, r.f] # {b, c, f} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + s1.update(s2) # s1 becomes {a, b, c, f}, gains ref to f + self.assertEqual(r._lrc, base_lrc - 3 + 4) # -3 for original a b c, +4 for new a b c f (but a b c are still borrowed, so net +1 for f) + + @unittest.expectedFailure + def test_union_update_adds_new_refs_2(self): + """ + |= should add references to new elements from s2 that + weren't already in s1, increasing the LRC accordingly. + Using |=. First set is in the region, second set is in local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.b, r.c, r.f] # {b, c, f} + + r.s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + r.s1 |= s2 + self.assertEqual(r._lrc, base_lrc) + + @unittest.expectedFailure + def test_union_update_adds_new_refs_2_update(self): + """ + |= should add references to new elements from s2 that + weren't already in s1, increasing the LRC accordingly. + Using update instead of |=. First set is in the region, second set is in local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.b, r.c, r.f] # {b, c, f} + + r.s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + r.s1.update(s2) + self.assertEqual(r._lrc, base_lrc) def test_union_update_released_decreases_lrc(self): """ @@ -909,6 +996,64 @@ def test_difference_update_removes_refs(self): s1 -= s2 # s1 becomes {b, c}, drops ref to a self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for original a b c, +2 for remaining b and c + def test_difference_update_removes_refs_difference_update(self): + """ + -= should release references to elements removed from s1, + decreasing the LRC by the number of elements subtracted. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.a] # {a} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + s1.difference_update(s2) # s1 becomes {b, c}, drops ref to a + self.assertEqual(r._lrc, base_lrc - 3 + 2) # -3 for original a b c, +2 for remaining b and c + + @unittest.expectedFailure + def test_difference_update_removes_refs_2(self): + """ + -= should release references to elements removed from s1, + decreasing the LRC by the number of elements subtracted. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.a] # {a} + + r.s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + r.s1 -= s2 # s1 becomes {b, c}, drops ref to a + self.assertEqual(r._lrc, base_lrc) # -3 for original a b c, +2 for remaining b and c + + def test_difference_update_removes_refs_difference_2_update(self): + """ + -= should release references to elements removed from s1, + decreasing the LRC by the number of elements subtracted. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.a] # {a} + + r.s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + r.s1.difference_update(s2) # s1 becomes {b, c}, drops ref to a + self.assertEqual(r._lrc, base_lrc) + def test_difference_update_result_released(self): """ Releasing s1 after -= should drop all remaining references it holds. @@ -979,6 +1124,45 @@ def test_symmetric_difference_update_operator_matches_method(self): s1_method.symmetric_difference_update(s2) s1_operator ^= s2 self.assertEqual(s1_method, s1_operator) + + @unittest.expectedFailure + def test_symmetric_difference_update_removes_refs_2(self): + """ + ^= should release references to elements removed from s1, + decreasing the LRC by the number of elements subtracted. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.a] # {a} + + r.s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + r.s1 ^= s2 + self.assertEqual(r._lrc, base_lrc) + + def test_symmetric_difference_update_removes_refs_difference_2_update(self): + """ + ^= should release references to elements removed from s1, + decreasing the LRC by the number of elements subtracted. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.a] # {a} + + r.s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + r.s1.symmetric_difference_update(s2) + self.assertEqual(r._lrc, base_lrc) class TestRegionSetSubsetSuperset(unittest.TestCase): diff --git a/test_code/set_test/setiand_test1.py b/test_code/set_test/setiand_test1.py index a691e2642bd7d2..0ebea834bac509 100644 --- a/test_code/set_test/setiand_test1.py +++ b/test_code/set_test/setiand_test1.py @@ -43,8 +43,8 @@ class A: pass; # print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1 # print(f"Intersection result: {s1}") -r.arr1 = [r.a, r.b, r.c] -arr2 = [r.a, r.f] +r.arr1 = [r.a, r.b, r.c, r.d, r.e] +arr2 = [r.a, r.d, r.f] print(f"Region after creating arr1 and arr2: {r}") # +3 r.s1 = set(r.arr1) print(f"Region after creating set1: {r}") # +0 @@ -53,8 +53,8 @@ class A: pass; print(f"Region after creating set2: {r}") # +3 print(f"{s2}") input("Press Enter to create set intersection...") -# r.s1 &= s2 -r.s1.intersection_update(s2) +r.s1 &= s2 +# r.s1.intersection_update(s2) print(f"Region after creating set intersection: {r}") print(f"Intersection result: {r.s1}") @@ -86,7 +86,7 @@ class A: pass; # print(f"Region after creating set2: {r}") # +0 # print(f"{r.s2}") # input("Press Enter to create set intersection...") -# s1 &= r.s2 -# # s1.intersection_update(r.s2) +# # s1 &= r.s2 +# s1.intersection_update(r.s2) # print(f"Region after creating set intersection: {r}") # print(f"Intersection result: {s1}") \ No newline at end of file diff --git a/test_code/set_test/setisub_test1.py b/test_code/set_test/setisub_test1.py index 981afbb0d866cc..90dc0e6128d19a 100644 --- a/test_code/set_test/setisub_test1.py +++ b/test_code/set_test/setisub_test1.py @@ -16,18 +16,32 @@ class A: pass; f = A() g = A() +# r.arr1 = [r.a, r.b, r.c] +# r.arr2 = [r.a] +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") +# print(f"{s1}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") +# print(f"{s2}") +# input("Press Enter to create set difference...") +# s1 -= s2 +# print(f"Region after creating set difference: {r}") +# print(f"{s1}") + r.arr1 = [r.a, r.b, r.c] r.arr2 = [r.a] -s1 = set(r.arr1) +r.s1 = set(r.arr1) print(f"Region after creating set1: {r}") -print(f"{s1}") +print(f"{r.s1}") s2 = set(r.arr2) print(f"Region after creating set2: {r}") print(f"{s2}") input("Press Enter to create set difference...") -s1 -= s2 +# r.s1 -= s2 +r.s1.difference_update(s2) print(f"Region after creating set difference: {r}") -print(f"{s1}") +print(f"{r.s1}") # s1 = set(r.arr1) # print(f"Region after creating set1: {r}") diff --git a/test_code/set_test/setixor_test1.py b/test_code/set_test/setixor_test1.py index 7ac5bd46006784..421f9a7d0e36c7 100644 --- a/test_code/set_test/setixor_test1.py +++ b/test_code/set_test/setixor_test1.py @@ -36,8 +36,8 @@ class A: pass; print(f"Region after creating set2: {r}") # +3 print(f"{s2}") input("Press Enter to create set xor...") -# s1 ^= s2 -r.s1.symmetric_difference_update(s2) # -4 for derefs r.a, r.b, r.c, r.f, then +1 for r.c +# r.s1 ^= s2 +r.s1.symmetric_difference_update(s2) print(f"Region after creating set xor: {r}") print(f"Xor result: {r.s1}") From 6be539fea2654f4a4f93cb1b182bf3900cd18ad2 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Tue, 10 Mar 2026 15:40:53 +0100 Subject: [PATCH 21/37] update test cases and unit test for set --- Lib/test/test_regions/test_set.py | 49 ++++++++++++++++++++++++++--- test_code/set_test/setior_test1.py | 49 +++++++++++------------------ test_code/set_test/setisub_test1.py | 4 +-- test_code/set_test/setor_test1.py | 21 ++++++++++--- test_code/set_test/setsub_test1.py | 2 +- 5 files changed, 82 insertions(+), 43 deletions(-) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index 1124342a2417a3..4ab383bafdb65f 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -246,6 +246,25 @@ def test_set_difference_result_releases_lrc_on_none(self): s4 = None self.assertLess(r._lrc, base_lrc) + def test_set_difference_result_releases_lrc_on_none_2_elem(self): + """ + Setting the result of a difference to None should release + any borrowed references it holds. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.a] + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + s3 = s1.difference(s2) + self.assertEqual(r._lrc, base_lrc + 2) + class TestRegionSetSymmetricDifference(unittest.TestCase): """Tests for symmetric difference (XOR) operations on sets.""" @@ -536,11 +555,8 @@ def test_intersection_operator_matches_method(self): s2 = set(r.arr2) base_lrc = r._lrc - result_method = s1.intersection(s2) - result_operator = s1 & s2 - self.assertEqual(result_method, result_operator) - self.assertEqual(r._lrc, base_lrc + 2 + 2) # both should borrow the same common elements, and there are two common elements, +2 from result_method and +2 from result_operator - + _ = s1 & s2 + self.assertEqual(r._lrc, base_lrc + 2) def test_intersection_multiple_sets(self): """ Intersection across three sets should only retain elements @@ -835,6 +851,29 @@ def test_union_lrc_reflects_all_unique_elements(self): result = None self.assertEqual(r._lrc, base_lrc) + def test_union_lrc_reflects_all_unique_elements_union(self): + """ + The union result holds references to all unique elements across + both sets, so the LRC should increase by the count of unique elements. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.f = self.A() + r.arr1 = [r.a, r.b, r.c] # {a, b, c} + r.arr2 = [r.b, r.c, r.f] # {b, c, f} + + s1 = set(r.arr1) + s2 = set(r.arr2) + base_lrc = r._lrc + + result = s1.union(s2) # {a, b, c, f} + self.assertEqual(r._lrc, base_lrc + 4) + + result = None + self.assertEqual(r._lrc, base_lrc) + def test_union_operator_matches_method(self): """ The `|` operator should behave identically to union(). diff --git a/test_code/set_test/setior_test1.py b/test_code/set_test/setior_test1.py index 8c7b495f742b94..e7f0ed75343a75 100644 --- a/test_code/set_test/setior_test1.py +++ b/test_code/set_test/setior_test1.py @@ -16,40 +16,27 @@ class A: pass; r.f = A() r.arr1 = [r.a, r.b, r.c] r.arr2 = [r.b, r.c, r.f] -s1 = set(r.arr1) + +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") +# print(f"{s1}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") +# print(f"{s2}") +# input("Press Enter to create set or...") +# s1 |= s2 +# # s1.update(s2) +# print(f"Region after creating set or: {r}") +# print(f"Or result: {s1}") + +r.s1 = set(r.arr1) print(f"Region after creating set1: {r}") -print(f"{s1}") +print(f"{r.s1}") s2 = set(r.arr2) print(f"Region after creating set2: {r}") print(f"{s2}") input("Press Enter to create set or...") -s1 |= s2 -# result = s1.symmetric_difference(s2) +# r.s1 |= s2 +r.s1.update(s2) print(f"Region after creating set or: {r}") -print(f"Or result: {s1}") - -# r.a = A() -# r.b = A() -# r.c = A() -# r2.d = A() -# r2.e = A() -# r2.f = A() -# r2.g = A() -# arr1 = [r.a, r.b, r.c, r2.d] -# arr2 = [r.c, r2.d, r2.e, r2.f, r2.g] -# print(f"Region1 before creating sets: {r}") -# print(f"Region2 before creating sets: {r2}") -# s1 = set(arr1) -# print(f"Region1 after creating set1: {r}") -# print(f"Region2 after creating set1: {r2}") -# print(f"{s1}") -# s2 = set(arr2) -# print(f"Region1 after creating set2: {r}") -# print(f"Region2 after creating set2: {r2}") -# print(f"{s2}") -# input("Press Enter to create set xor...") -# result = s1 ^ s2 -# # result = s1.symmetric_difference(s2) -# print(f"Region1 after creating set xor: {r}") -# print(f"Region2 after creating set xor: {r2}") -# print(f"Xor result: {result}") \ No newline at end of file +print(f"Or result: {r.s1}") \ No newline at end of file diff --git a/test_code/set_test/setisub_test1.py b/test_code/set_test/setisub_test1.py index 90dc0e6128d19a..14b6fb270373fa 100644 --- a/test_code/set_test/setisub_test1.py +++ b/test_code/set_test/setisub_test1.py @@ -38,8 +38,8 @@ class A: pass; print(f"Region after creating set2: {r}") print(f"{s2}") input("Press Enter to create set difference...") -# r.s1 -= s2 -r.s1.difference_update(s2) +r.s1 -= s2 +# r.s1.difference_update(s2) print(f"Region after creating set difference: {r}") print(f"{r.s1}") diff --git a/test_code/set_test/setor_test1.py b/test_code/set_test/setor_test1.py index 970ef42d7ff3d2..8ae81dcf5fe032 100644 --- a/test_code/set_test/setor_test1.py +++ b/test_code/set_test/setor_test1.py @@ -16,14 +16,27 @@ class A: pass; r.f = A() r.arr1 = [r.a, r.b, r.c] r.arr2 = [r.b, r.c, r.f] -s1 = set(r.arr1) + +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") +# print(f"{s1}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") +# print(f"{s2}") +# input("Press Enter to create set or...") +# # result = s1 | s2 +# result = s1.union(s2) +# print(f"Region after creating set or: {r}") +# print(f"Or result: {result}") + +r.s1 = set(r.arr1) print(f"Region after creating set1: {r}") -print(f"{s1}") +print(f"{r.s1}") s2 = set(r.arr2) print(f"Region after creating set2: {r}") print(f"{s2}") input("Press Enter to create set or...") -result = s1 | s2 -# result = s1.union(s2) +result = r.s1 | s2 +# result = r.s1.union(s2) print(f"Region after creating set or: {r}") print(f"Or result: {result}") \ No newline at end of file diff --git a/test_code/set_test/setsub_test1.py b/test_code/set_test/setsub_test1.py index e4e1416e60aa2e..1eb071758fd16d 100644 --- a/test_code/set_test/setsub_test1.py +++ b/test_code/set_test/setsub_test1.py @@ -29,7 +29,7 @@ class A: pass; print(f"Region after creating set3: {r}") print(f"{s3}") input("Press Enter to create set difference...") -s4 = s1.difference(s2, s3) +s4 = s1.difference(s2) print(f"Region after creating set difference: {r}") print(f"{s4}") From 7a03554023a78a1b97d42203c2407b97ff5253f4 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Wed, 11 Mar 2026 11:46:37 +0100 Subject: [PATCH 22/37] update setior and update to work correctly. set_swap_bodies still has a problem, but commit as a progress update. Update unit test and test case for setior --- Lib/test/test_regions/test_set.py | 1 - Objects/setobject.c | 168 ++++++++++++++++++++++------- test_code/set_test/setior_test1.py | 8 +- 3 files changed, 135 insertions(+), 42 deletions(-) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index 4ab383bafdb65f..4ba35cb96ac515 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -964,7 +964,6 @@ def test_union_update_adds_new_refs_2(self): r.s1 |= s2 self.assertEqual(r._lrc, base_lrc) - @unittest.expectedFailure def test_union_update_adds_new_refs_2_update(self): """ |= should add references to new elements from s2 that diff --git a/Objects/setobject.c b/Objects/setobject.c index 15116182564b79..da1be5abd52c06 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -276,7 +276,9 @@ The caller is responsible for updating the key's reference count and the setobject's fill and used fields. */ static int -set_insert_clean(PySetObject* set, setentry *table, size_t mask, PyObject *key, Py_hash_t hash) +set_insert_clean( + PySetObject* set, setentry *table, size_t mask, + PyObject *key, Py_hash_t hash, bool take_ref) { setentry *entry; size_t perturb = hash; @@ -298,8 +300,10 @@ set_insert_clean(PySetObject* set, setentry *table, size_t mask, PyObject *key, i = (i * 5 + 1 + perturb) & mask; } found_null: - if(PyRegion_TakeRef(set, key)) { - return -1; + if (take_ref) { + if (PyRegion_TakeRef(set, key)) { + return -1; + } } entry->key = key; entry->hash = hash; @@ -377,7 +381,7 @@ set_table_resize(PySetObject *so, Py_ssize_t minused) for (entry = oldtable; entry <= oldtable + oldmask; entry++) { if (entry->key != NULL) { // should never fail since it already has an element. - int res = set_insert_clean(so, newtable, newmask, entry->key, entry->hash); + int res = set_insert_clean(so, newtable, newmask, entry->key, entry->hash, false); assert(res==0); } } @@ -385,7 +389,7 @@ set_table_resize(PySetObject *so, Py_ssize_t minused) so->fill = so->used; for (entry = oldtable; entry <= oldtable + oldmask; entry++) { if (entry->key != NULL && entry->key != dummy) { - int res = set_insert_clean(so, newtable, newmask, entry->key, entry->hash); + int res = set_insert_clean(so, newtable, newmask, entry->key, entry->hash, false); assert(res==0); } } @@ -758,7 +762,7 @@ set_merge_lock_held(PySetObject *so, PyObject *otherset) if(PyRegion_AddLocalRef(key)) { return -1; } - if(set_insert_clean(so, newtable, newmask, Py_NewRef(key),other_entry->hash)) { + if(set_insert_clean(so, newtable, newmask, Py_NewRef(key),other_entry->hash, true)) { return -1; } } @@ -1343,45 +1347,132 @@ set_new(PyTypeObject *type, PyObject *args, PyObject *kwds) result to be swapped into one of the original inputs). */ -static int +static void set_swap_bodies(PySetObject *a, PySetObject *b) { - Py_ssize_t t; - setentry *u; - setentry tab[PySet_MINSIZE]; - Py_hash_t h; - + // if a and b are in the same region, no problem. use this - t = a->fill; a->fill = b->fill; b->fill = t; - t = a->used; - FT_ATOMIC_STORE_SSIZE_RELAXED(a->used, b->used); - FT_ATOMIC_STORE_SSIZE_RELAXED(b->used, t); - t = a->mask; a->mask = b->mask; b->mask = t; - - u = a->table; - if (a->table == a->smalltable) + if(PyRegion_ShareRegion(a,b)){ + Py_ssize_t t; + setentry *u; + setentry tab[PySet_MINSIZE]; + Py_hash_t h; + t = a->fill; a->fill = b->fill; b->fill = t; + t = a->used; + FT_ATOMIC_STORE_SSIZE_RELAXED(a->used, b->used); + FT_ATOMIC_STORE_SSIZE_RELAXED(b->used, t); + t = a->mask; a->mask = b->mask; b->mask = t; + + u = a->table; + if (a->table == a->smalltable) u = b->smalltable; - a->table = b->table; - if (b->table == b->smalltable) + a->table = b->table; + if (b->table == b->smalltable) a->table = a->smalltable; - b->table = u; + b->table = u; + + if (a->table == a->smalltable || b->table == b->smalltable) { + memcpy(tab, a->smalltable, sizeof(tab)); + memcpy(a->smalltable, b->smalltable, sizeof(tab)); + memcpy(b->smalltable, tab, sizeof(tab)); + } + + if (PyType_IsSubtype(Py_TYPE(a), &PyFrozenSet_Type) && + PyType_IsSubtype(Py_TYPE(b), &PyFrozenSet_Type)) { + h = FT_ATOMIC_LOAD_SSIZE_RELAXED(a->hash); + FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, FT_ATOMIC_LOAD_SSIZE_RELAXED(b->hash)); + FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, h); + } else { + FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, -1); + FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, -1); + } + } + else { + setentry *entry; + Py_ssize_t pos; + Py_ssize_t i; - if (a->table == a->smalltable || b->table == b->smalltable) { - memcpy(tab, a->smalltable, sizeof(tab)); - memcpy(a->smalltable, b->smalltable, sizeof(tab)); - memcpy(b->smalltable, tab, sizeof(tab)); - } + Py_ssize_t a_snapped = 0; + Py_ssize_t b_snapped = 0; + Py_ssize_t a_inserted = 0; + Py_ssize_t b_inserted = 0; - if (PyType_IsSubtype(Py_TYPE(a), &PyFrozenSet_Type) && - PyType_IsSubtype(Py_TYPE(b), &PyFrozenSet_Type)) { - h = FT_ATOMIC_LOAD_SSIZE_RELAXED(a->hash); - FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, FT_ATOMIC_LOAD_SSIZE_RELAXED(b->hash)); - FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, h); - } else { - FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, -1); - FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, -1); + // 1. Allocate flat arrays to hold keys and hashes from 'a' and 'b' + Py_ssize_t a_size = a->used; + Py_ssize_t b_size = b->used; + + PyObject **a_keys = PyMem_New(PyObject *, a_size); + Py_hash_t *a_hashes = PyMem_New(Py_hash_t, a_size); + PyObject **b_keys = PyMem_New(PyObject *, b_size); + Py_hash_t *b_hashes = PyMem_New(Py_hash_t, b_size); + + if (!a_keys || !a_hashes || !b_keys || !b_hashes) + goto error; + + // 2. Snapshot 'a' keys + pos = 0; i = 0; + while (set_next(a, &pos, &entry)) { + a_keys[i] = PyRegion_NewRef(entry->key); + a_hashes[i] = entry->hash; + i++; + a_snapped++; + } + + // 3. Snapshot 'b' keys + pos = 0; i = 0; + while (set_next(b, &pos, &entry)) { + b_keys[i] = PyRegion_NewRef(entry->key); + b_hashes[i] = entry->hash; + i++; + b_snapped++; + } + + // // 4. Call RemoveRef on all keys in 'a' and 'b' + // for (i = 0; i < a_size; i++){ + // PyRegion_RemoveRef(a, a_keys[i]); + // Py_DECREF(a_keys[i]); + // } + // for (i = 0; i < b_size; i++){ + // PyRegion_RemoveRef(b, b_keys[i]); + // Py_DECREF(b_keys[i]); + // } + + // 5. Clear both sets + set_clear_internal((PyObject *)a); + set_clear_internal((PyObject *)b); + + // 6. Fill 'a' with 'b's original keys, 'b' with 'a's original keys + for (i = 0; i < b_size; i++) { + if (set_add_entry(a, b_keys[i], b_hashes[i]) < 0) + goto error; + PyRegion_RemoveLocalRef(b_keys[i]); + Py_DECREF(b_keys[i]); + a_inserted++; + } + for (i = 0; i < a_size; i++) { + if (set_add_entry(b, a_keys[i], a_hashes[i]) < 0) + goto error; + PyRegion_RemoveLocalRef(a_keys[i]); + Py_DECREF(a_keys[i]); + b_inserted++; + } + + error: + // on success: a_inserted == b_snapped and b_inserted == a_snapped + // so these loops are no-ops on the success path + for (i = a_inserted; i < b_snapped; i++) { + PyRegion_RemoveLocalRef(b_keys[i]); + Py_DECREF(b_keys[i]); + } + for (i = b_inserted; i < a_snapped; i++) { + PyRegion_RemoveLocalRef(a_keys[i]); + Py_DECREF(a_keys[i]); + } + PyMem_Free(a_keys); + PyMem_Free(a_hashes); + PyMem_Free(b_keys); + PyMem_Free(b_hashes); } - // else, use 4 barriers before swapping } /*[clinic input] @@ -1666,6 +1757,7 @@ set_intersection_update(PySetObject *so, PyObject *other) tmp = set_intersection(so, other); if (tmp == NULL) return NULL; + PyRegion_IsLocal(tmp); set_swap_bodies(so, (PySetObject *)tmp); PyRegion_RemoveLocalRef(tmp); Py_DECREF(tmp); @@ -1691,6 +1783,7 @@ set_intersection_update_multi_impl(PySetObject *so, PyObject * const *others, if (tmp == NULL) return NULL; Py_BEGIN_CRITICAL_SECTION(so); + PyRegion_IsLocal(tmp); set_swap_bodies(so, (PySetObject *)tmp); Py_END_CRITICAL_SECTION(); PyRegion_RemoveLocalRef(tmp); @@ -1731,6 +1824,7 @@ set_iand(PyObject *self, PyObject *other) PyRegion_RemoveLocalRef(result); Py_DECREF(result); return PyRegion_NewRef(so); + // return Py_NewRef(so); } /*[clinic input] diff --git a/test_code/set_test/setior_test1.py b/test_code/set_test/setior_test1.py index e7f0ed75343a75..cc0ddd818ac2ef 100644 --- a/test_code/set_test/setior_test1.py +++ b/test_code/set_test/setior_test1.py @@ -14,8 +14,8 @@ class A: pass; r.d = A() r.e = A() r.f = A() -r.arr1 = [r.a, r.b, r.c] -r.arr2 = [r.b, r.c, r.f] +r.arr1 = [r.a, r.b, r.c, r.d] +r.arr2 = [r.b, r.c, r.f, r.e] # s1 = set(r.arr1) # print(f"Region after creating set1: {r}") @@ -31,10 +31,10 @@ class A: pass; r.s1 = set(r.arr1) print(f"Region after creating set1: {r}") -print(f"{r.s1}") +# print(f"{r.s1}") s2 = set(r.arr2) print(f"Region after creating set2: {r}") -print(f"{s2}") +# print(f"{s2}") input("Press Enter to create set or...") # r.s1 |= s2 r.s1.update(s2) From 845327defb0ec315b7530b8844be55370e34bf25 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Wed, 11 Mar 2026 16:04:46 +0100 Subject: [PATCH 23/37] update set_swap_body --- Objects/setobject.c | 164 ++++++++-------------------- test_code/set_test/setiand_test1.py | 7 +- 2 files changed, 49 insertions(+), 122 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index da1be5abd52c06..3eeb9345e80f40 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1346,132 +1346,58 @@ set_new(PyTypeObject *type, PyObject *args, PyObject *kwds) Useful for operations that update in-place (by allowing an intermediate result to be swapped into one of the original inputs). */ +static PyObject *set_difference(PySetObject *, PyObject *); // add this static void set_swap_bodies(PySetObject *a, PySetObject *b) { - - // if a and b are in the same region, no problem. use this - if(PyRegion_ShareRegion(a,b)){ - Py_ssize_t t; - setentry *u; - setentry tab[PySet_MINSIZE]; - Py_hash_t h; - t = a->fill; a->fill = b->fill; b->fill = t; - t = a->used; - FT_ATOMIC_STORE_SSIZE_RELAXED(a->used, b->used); - FT_ATOMIC_STORE_SSIZE_RELAXED(b->used, t); - t = a->mask; a->mask = b->mask; b->mask = t; - - u = a->table; - if (a->table == a->smalltable) - u = b->smalltable; - a->table = b->table; - if (b->table == b->smalltable) - a->table = a->smalltable; - b->table = u; - - if (a->table == a->smalltable || b->table == b->smalltable) { - memcpy(tab, a->smalltable, sizeof(tab)); - memcpy(a->smalltable, b->smalltable, sizeof(tab)); - memcpy(b->smalltable, tab, sizeof(tab)); - } - - if (PyType_IsSubtype(Py_TYPE(a), &PyFrozenSet_Type) && - PyType_IsSubtype(Py_TYPE(b), &PyFrozenSet_Type)) { - h = FT_ATOMIC_LOAD_SSIZE_RELAXED(a->hash); - FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, FT_ATOMIC_LOAD_SSIZE_RELAXED(b->hash)); - FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, h); - } else { - FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, -1); - FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, -1); - } - } - else { + if(!PyRegion_IsLocal(a)) { + Py_ssize_t pos = 0; setentry *entry; - Py_ssize_t pos; - Py_ssize_t i; - - Py_ssize_t a_snapped = 0; - Py_ssize_t b_snapped = 0; - Py_ssize_t a_inserted = 0; - Py_ssize_t b_inserted = 0; + PyObject *c = set_difference(a, b); - // 1. Allocate flat arrays to hold keys and hashes from 'a' and 'b' - Py_ssize_t a_size = a->used; - Py_ssize_t b_size = b->used; - - PyObject **a_keys = PyMem_New(PyObject *, a_size); - Py_hash_t *a_hashes = PyMem_New(Py_hash_t, a_size); - PyObject **b_keys = PyMem_New(PyObject *, b_size); - Py_hash_t *b_hashes = PyMem_New(Py_hash_t, b_size); - - if (!a_keys || !a_hashes || !b_keys || !b_hashes) - goto error; - - // 2. Snapshot 'a' keys - pos = 0; i = 0; - while (set_next(a, &pos, &entry)) { - a_keys[i] = PyRegion_NewRef(entry->key); - a_hashes[i] = entry->hash; - i++; - a_snapped++; - } - - // 3. Snapshot 'b' keys - pos = 0; i = 0; - while (set_next(b, &pos, &entry)) { - b_keys[i] = PyRegion_NewRef(entry->key); - b_hashes[i] = entry->hash; - i++; - b_snapped++; - } - - // // 4. Call RemoveRef on all keys in 'a' and 'b' - // for (i = 0; i < a_size; i++){ - // PyRegion_RemoveRef(a, a_keys[i]); - // Py_DECREF(a_keys[i]); - // } - // for (i = 0; i < b_size; i++){ - // PyRegion_RemoveRef(b, b_keys[i]); - // Py_DECREF(b_keys[i]); - // } - - // 5. Clear both sets - set_clear_internal((PyObject *)a); - set_clear_internal((PyObject *)b); - - // 6. Fill 'a' with 'b's original keys, 'b' with 'a's original keys - for (i = 0; i < b_size; i++) { - if (set_add_entry(a, b_keys[i], b_hashes[i]) < 0) - goto error; - PyRegion_RemoveLocalRef(b_keys[i]); - Py_DECREF(b_keys[i]); - a_inserted++; - } - for (i = 0; i < a_size; i++) { - if (set_add_entry(b, a_keys[i], a_hashes[i]) < 0) - goto error; - PyRegion_RemoveLocalRef(a_keys[i]); - Py_DECREF(a_keys[i]); - b_inserted++; + while (set_next(c, &pos, &entry)) { + PyObject *key = entry->key; + int rv = PyRegion_AddLocalRef(key); // b now points to "key" because of the swap, and b is guaranteed to be in the local region, so we can add a local reference to key for b. + assert(rv==0); // assure that AddLocalRef cannot fail since key is already in the region. + PyRegion_RemoveRef(a, key); } - - error: - // on success: a_inserted == b_snapped and b_inserted == a_snapped - // so these loops are no-ops on the success path - for (i = a_inserted; i < b_snapped; i++) { - PyRegion_RemoveLocalRef(b_keys[i]); - Py_DECREF(b_keys[i]); - } - for (i = b_inserted; i < a_snapped; i++) { - PyRegion_RemoveLocalRef(a_keys[i]); - Py_DECREF(a_keys[i]); - } - PyMem_Free(a_keys); - PyMem_Free(a_hashes); - PyMem_Free(b_keys); - PyMem_Free(b_hashes); + assert(PyRegion_IsLocal(c)); + Py_DECREF(c); // set_dealloc will remove all references from c to the objects in the set, and set_dealloc also handles LRC, so we don't have to do anything else to remove the references from the region here. + } + + Py_ssize_t t; + setentry *u; + setentry tab[PySet_MINSIZE]; + Py_hash_t h; + t = a->fill; a->fill = b->fill; b->fill = t; + t = a->used; + FT_ATOMIC_STORE_SSIZE_RELAXED(a->used, b->used); + FT_ATOMIC_STORE_SSIZE_RELAXED(b->used, t); + t = a->mask; a->mask = b->mask; b->mask = t; + + u = a->table; + if (a->table == a->smalltable) + u = b->smalltable; + a->table = b->table; + if (b->table == b->smalltable) + a->table = a->smalltable; + b->table = u; + + if (a->table == a->smalltable || b->table == b->smalltable) { + memcpy(tab, a->smalltable, sizeof(tab)); + memcpy(a->smalltable, b->smalltable, sizeof(tab)); + memcpy(b->smalltable, tab, sizeof(tab)); + } + + if (PyType_IsSubtype(Py_TYPE(a), &PyFrozenSet_Type) && + PyType_IsSubtype(Py_TYPE(b), &PyFrozenSet_Type)) { + h = FT_ATOMIC_LOAD_SSIZE_RELAXED(a->hash); + FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, FT_ATOMIC_LOAD_SSIZE_RELAXED(b->hash)); + FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, h); + } else { + FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, -1); + FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, -1); } } diff --git a/test_code/set_test/setiand_test1.py b/test_code/set_test/setiand_test1.py index 0ebea834bac509..21ea18c10667d9 100644 --- a/test_code/set_test/setiand_test1.py +++ b/test_code/set_test/setiand_test1.py @@ -44,7 +44,7 @@ class A: pass; # print(f"Intersection result: {s1}") r.arr1 = [r.a, r.b, r.c, r.d, r.e] -arr2 = [r.a, r.d, r.f] +arr2 = [r.a, r.b, r.c, r.f] print(f"Region after creating arr1 and arr2: {r}") # +3 r.s1 = set(r.arr1) print(f"Region after creating set1: {r}") # +0 @@ -53,10 +53,11 @@ class A: pass; print(f"Region after creating set2: {r}") # +3 print(f"{s2}") input("Press Enter to create set intersection...") -r.s1 &= s2 -# r.s1.intersection_update(s2) +# r.s1 &= s2 +r.s1.intersection_update(s2) print(f"Region after creating set intersection: {r}") print(f"Intersection result: {r.s1}") +print(f"s2: {s2}") # r.arr1 = [r.a, r.b, r.c] # arr2 = [r.a, r.b, r.f] From 7db8d92bcee9f129597113bc58a02a9231917071 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Thu, 12 Mar 2026 13:58:15 +0100 Subject: [PATCH 24/37] update set_swap_bodies comment --- Objects/setobject.c | 32 ++++++++++++++++- test_code/set_test/setiand_test1.py | 55 ++++++++++++++--------------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index 3eeb9345e80f40..b0a88397d48c97 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1345,12 +1345,42 @@ set_new(PyTypeObject *type, PyObject *args, PyObject *kwds) The function always succeeds and it leaves both objects in a stable state. Useful for operations that update in-place (by allowing an intermediate result to be swapped into one of the original inputs). + + This function is only used in set_intersection_update_multi_impl and set_intersection_update. + In other word, only .intersection_update and &= use this function. */ static PyObject *set_difference(PySetObject *, PyObject *); // add this static void set_swap_bodies(PySetObject *a, PySetObject *b) { + /* + The object b is guarantee to be the local object, due to the origin from set_intersection_update_multi_impl and set_intersection_update. + If the object a is the local object as well, no barrier is needed. For example, + + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + s1 = set(r.arr1) + s2 = set(r.arr2) + + In set_intersection_update_multi_impl, tmp, which is b when it is passed to this function, points to r.b and r.c when being created, which increases LRC by 2. + After swapping, tmp points to r.a, r.b, and r.c. When tmp ref count is decreased to 0, LRC is also decreased by 3 from set_dealloc. + Finally, the result of s1.intersect_update(s2) seems as s1 releases the reference to r.a, which means LRC is finally decreased by one as expected. + + However, if s1 is in the region, which mean a is also in the region, not the local region anymore. + + r.arr1 = [r.a, r.b, r.c] + r.arr2 = [r.b, r.c, r.f] + r.s1 = set(r.arr1) + s2 = set(r.arr2) + + If there is no additional mechanism to handle LRC, the final LRC will be lower than it should be. + Without the mechanism, when tmp is created, LRC is increased by 2. Then, after swapping, when destroying tmp, LRC is decreased by 3. The final LRC is decreased by 1. + However, LRC should not be changed in this case because s1 is in the region. Releasing and pointing to the objects in the same region should not modify the LRC. + So, the solution is that after increasing LRC by 2 from creating tmp, which points to the intersection results, we also increase LRC by the size of "a-b", which is 1. + Then, we can use the original swap mechanism. Finally, when tmp is destroyed, LRC is decreased by 3. +3 and -3 becomes zero. Correct! + + */ if(!PyRegion_IsLocal(a)) { Py_ssize_t pos = 0; setentry *entry; @@ -1360,7 +1390,7 @@ set_swap_bodies(PySetObject *a, PySetObject *b) PyObject *key = entry->key; int rv = PyRegion_AddLocalRef(key); // b now points to "key" because of the swap, and b is guaranteed to be in the local region, so we can add a local reference to key for b. assert(rv==0); // assure that AddLocalRef cannot fail since key is already in the region. - PyRegion_RemoveRef(a, key); + PyRegion_RemoveRef(a, key); // Because key will be swapped, and b will be the parent instead. So, remove the relationship between a and key. } assert(PyRegion_IsLocal(c)); Py_DECREF(c); // set_dealloc will remove all references from c to the objects in the set, and set_dealloc also handles LRC, so we don't have to do anything else to remove the references from the region here. diff --git a/test_code/set_test/setiand_test1.py b/test_code/set_test/setiand_test1.py index 21ea18c10667d9..fbc9450b034adc 100644 --- a/test_code/set_test/setiand_test1.py +++ b/test_code/set_test/setiand_test1.py @@ -15,19 +15,19 @@ class A: pass; r.e = A() r.f = A() -# r.arr1 = [r.a, r.b, r.c] -# r.arr2 = [r.b, r.c, r.f] -# s1 = set(r.arr1) -# print(f"Region after creating set1: {r}") # +3 -# print(f"{s1}") -# s2 = set(r.arr2) -# print(f"Region after creating set2: {r}") # +3 -# print(f"{s2}") -# input("Press Enter to create set intersection...") -# # s1 &= s2 -# s1.intersection_update(s2) -# print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1, then +2: add the reference from s1 to the result -# print(f"Intersection result: {s1}") +r.arr1 = [r.a, r.b, r.c] +r.arr2 = [r.b, r.c, r.f] +s1 = set(r.arr1) +print(f"Region after creating set1: {r}") # +3 +print(f"{s1}") +s2 = set(r.arr2) +print(f"Region after creating set2: {r}") # +3 +print(f"{s2}") +input("Press Enter to create set intersection...") +# s1 &= s2 +s1.intersection_update(s2) +print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1, then +2: add the reference from s1 to the result +print(f"Intersection result: {s1}") # r.arr1 = [r.a, r.b, r.c] # r.arr2 = [r.f] @@ -43,21 +43,20 @@ class A: pass; # print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1 # print(f"Intersection result: {s1}") -r.arr1 = [r.a, r.b, r.c, r.d, r.e] -arr2 = [r.a, r.b, r.c, r.f] -print(f"Region after creating arr1 and arr2: {r}") # +3 -r.s1 = set(r.arr1) -print(f"Region after creating set1: {r}") # +0 -print(f"{r.s1}") -s2 = set(arr2) -print(f"Region after creating set2: {r}") # +3 -print(f"{s2}") -input("Press Enter to create set intersection...") -# r.s1 &= s2 -r.s1.intersection_update(s2) -print(f"Region after creating set intersection: {r}") -print(f"Intersection result: {r.s1}") -print(f"s2: {s2}") +# r.arr1 = [r.a, r.b, r.c, r.d, r.e] +# arr2 = [r.a, r.b, r.c, r.f] +# print(f"Region after creating arr1 and arr2: {r}") # +3 +# r.s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") # +0 +# print(f"{r.s1}") +# s2 = set(arr2) +# print(f"Region after creating set2: {r}") # +3 +# print(f"{s2}") +# input("Press Enter to create set intersection...") +# # r.s1 &= s2 +# r.s1.intersection_update(s2) +# print(f"Region after creating set intersection: {r}") +# print(f"Intersection result: {r.s1}") # r.arr1 = [r.a, r.b, r.c] # arr2 = [r.a, r.b, r.f] From f62f8295d665262ca71632d6a5e50a56f3493d6a Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Mon, 16 Mar 2026 17:57:53 +0100 Subject: [PATCH 25/37] update test code --- test_code/set_test/add_set_to_region.py | 18 +++++++++ test_code/set_test/setand_test1.py | 46 +++++++++++++++------- test_code/set_test/setiand_test1.py | 51 +++++++++++++------------ test_code/set_test/setior_test1.py | 4 +- 4 files changed, 78 insertions(+), 41 deletions(-) create mode 100644 test_code/set_test/add_set_to_region.py diff --git a/test_code/set_test/add_set_to_region.py b/test_code/set_test/add_set_to_region.py new file mode 100644 index 00000000000000..d5d91addfcb844 --- /dev/null +++ b/test_code/set_test/add_set_to_region.py @@ -0,0 +1,18 @@ +from regions import Region +from immutable import freeze + +class A: pass +freeze(A()) + +r = Region() +r.a = A() +r.b = A() +r.c = A() + +print(f"Initial Region: {r}") +input("Press Enter to create set from region attributes...") +arr = set([r.a, r.b, r.c]) +print(f"Region after creating set: {r}") +input("Press Enter to set arr to region...") +r.arr = arr +print(f"Region after setting arr to set: {r}") \ No newline at end of file diff --git a/test_code/set_test/setand_test1.py b/test_code/set_test/setand_test1.py index e8b4eb984dba0b..720f42452a07b9 100644 --- a/test_code/set_test/setand_test1.py +++ b/test_code/set_test/setand_test1.py @@ -16,6 +16,7 @@ class A: pass; r1.d = A() r1.e = A() r1.f = A() +g = A() r1.arr1 = [r1.a, r1.b, r1.c] r1.arr2 = [r1.b, r1.c, r1.f] r1.arr3 = [r1.b, r1.d, r1.e] @@ -33,6 +34,21 @@ class A: pass; # print(f"Region after creating set intersection: {r}") # print(f"Intersection result: {result}") +print(f"Initial Region: {r1}") +r1.s1 = set(r1.arr1) +print(f"Region after creating set1: {r1}") +print(f"{r1.s1}") +s2 = set(r1.arr2) +print(f"Region after creating set2: {r1}") +print(f"{s2}") +input("Press Enter to create set intersection...") +# result = s1 & s2 +result = r1.s1.intersection(s2) +print(f"Region after creating set intersection: {r1}") +print(f"Intersection result: {result}") +result = g +print(f"Region after changing result: {r1}") + # print(f"Initial Region: {r1}") # s1 = set(r1.arr1) # print(f"Region after creating set1: {r1}") @@ -48,20 +64,22 @@ class A: pass; # print(f"Region after creating set intersection: {r1}") # print(f"Intersection result: {result}") -print(f"Initial Region: {r1}") -r1.s1 = set(r1.arr1) -print(f"Region after creating set1: {r1}") -print(f"{r1.s1}") -s2 = set(r1.arr2) -print(f"Region after creating set2: {r1}") -print(f"{s2}") -r1.s3 = set(r1.arr3) -print(f"Region after creating set3: {r1}") -print(f"{r1.s3}") -input("Press Enter to create set intersection...") -result = r1.s1.intersection(s2, r1.s3) -print(f"Region after creating set intersection: {r1}") -print(f"Intersection result: {result}") +# print(f"Initial Region: {r1}") +# r1.s1 = set(r1.arr1) +# print(f"Region after creating set1: {r1}") +# print(f"{r1.s1}") +# s2 = set(r1.arr2) +# print(f"Region after creating set2: {r1}") +# print(f"{s2}") +# r1.s3 = set(r1.arr3) +# print(f"Region after creating set3: {r1}") +# print(f"{r1.s3}") +# input("Press Enter to create set intersection...") +# result = r1.s1.intersection(s2, r1.s3) +# print(f"Region after creating set intersection: {r1}") +# print(f"Intersection result: {result}") +# result = g +# print(f"Region after changing result: {r1}") # print(f"Initial Region: {r1}") # print(f"Region r2: {r2}") diff --git a/test_code/set_test/setiand_test1.py b/test_code/set_test/setiand_test1.py index fbc9450b034adc..077d0c9df481f9 100644 --- a/test_code/set_test/setiand_test1.py +++ b/test_code/set_test/setiand_test1.py @@ -15,19 +15,19 @@ class A: pass; r.e = A() r.f = A() -r.arr1 = [r.a, r.b, r.c] -r.arr2 = [r.b, r.c, r.f] -s1 = set(r.arr1) -print(f"Region after creating set1: {r}") # +3 -print(f"{s1}") -s2 = set(r.arr2) -print(f"Region after creating set2: {r}") # +3 -print(f"{s2}") -input("Press Enter to create set intersection...") +# r.arr1 = [r.a, r.b, r.c] +# r.arr2 = [r.b, r.c, r.f] +# s1 = set(r.arr1) +# print(f"Region after creating set1: {r}") # +3 +# print(f"{s1}") +# s2 = set(r.arr2) +# print(f"Region after creating set2: {r}") # +3 +# print(f"{s2}") +# input("Press Enter to create set intersection...") # s1 &= s2 -s1.intersection_update(s2) -print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1, then +2: add the reference from s1 to the result -print(f"Intersection result: {s1}") +# # s1.intersection_update(s2) +# print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1, then +2: add the reference from s1 to the result +# print(f"Intersection result: {s1}") # r.arr1 = [r.a, r.b, r.c] # r.arr2 = [r.f] @@ -43,20 +43,21 @@ class A: pass; # print(f"Region after creating set intersection: {r}") # -3: eliminate all points from s1 # print(f"Intersection result: {s1}") -# r.arr1 = [r.a, r.b, r.c, r.d, r.e] -# arr2 = [r.a, r.b, r.c, r.f] -# print(f"Region after creating arr1 and arr2: {r}") # +3 -# r.s1 = set(r.arr1) -# print(f"Region after creating set1: {r}") # +0 -# print(f"{r.s1}") -# s2 = set(arr2) -# print(f"Region after creating set2: {r}") # +3 -# print(f"{s2}") -# input("Press Enter to create set intersection...") -# # r.s1 &= s2 +r.arr1 = [r.a, r.b, r.c, r.d, r.e] +arr2 = [r.a, r.b, r.c, r.f] +print(f"Region after creating arr1 and arr2: {r}") # +3 +r.s1 = set(r.arr1) +print(f"Region after creating set1: {r}") # +0 +print(f"{r.s1}") +input("Press Enter to create set1...") +s2 = set(arr2) +print(f"Region after creating set2: {r}") # +3 +print(f"{s2}") +input("Press Enter to create set intersection...") +r.s1 &= s2 # r.s1.intersection_update(s2) -# print(f"Region after creating set intersection: {r}") -# print(f"Intersection result: {r.s1}") +print(f"Region after creating set intersection: {r}") +print(f"Intersection result: {r.s1}") # r.arr1 = [r.a, r.b, r.c] # arr2 = [r.a, r.b, r.f] diff --git a/test_code/set_test/setior_test1.py b/test_code/set_test/setior_test1.py index cc0ddd818ac2ef..d1bf854e626568 100644 --- a/test_code/set_test/setior_test1.py +++ b/test_code/set_test/setior_test1.py @@ -36,7 +36,7 @@ class A: pass; print(f"Region after creating set2: {r}") # print(f"{s2}") input("Press Enter to create set or...") -# r.s1 |= s2 -r.s1.update(s2) +r.s1 |= s2 +# r.s1.update(s2) print(f"Region after creating set or: {r}") print(f"Or result: {r.s1}") \ No newline at end of file From 0b41cc31ffaa8743607495c683002bfda35ec14a Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Mon, 23 Mar 2026 13:49:21 +0100 Subject: [PATCH 26/37] PyObject_SelfIter should be migrated. Add test for this in test_set. Clean code in rangeobject.c --- Lib/test/test_regions/test_set.py | 36 +++++++++++++++++++++++++++++++ Objects/object.c | 4 +--- Objects/rangeobject.c | 6 ++---- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index 4ba35cb96ac515..3af6642e2a8fa5 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -135,6 +135,42 @@ def test_set_move_into_region_fails_if_element_in_another_region(self): self.assertTrue(is_local(ab)) self.assertTrue(is_local(ac)) + def test_iterator(self): + """ + Creating an iterator from a set that borrows from a region should + not increase the LRC, since the iterator itself does not hold + references to the elements (it borrows from the set). + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + s = set(r.arr) + base_lrc = r._lrc + + r.it = iter(s) + self.assertEqual(r._lrc, base_lrc-1) # s is moved into the region. + it2 = iter(s) + self.assertEqual(r._lrc, base_lrc) + + def test_iterator2(self): + """ + Creating an iterator from a set that borrows from a region should + not increase the LRC, since the iterator itself does not hold + references to the elements (it borrows from the set). + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + r.s = set(r.arr) + base_lrc = r._lrc + + it = iter(r.s) + self.assertEqual(r._lrc, base_lrc+1) # r.s is moved into the region. + it2 = iter(r.s) + self.assertEqual(r._lrc, base_lrc+2) + class TestRegionSetDiscard(unittest.TestCase): """Tests for set discard/pop operations and their effect on LRC.""" diff --git a/Objects/object.c b/Objects/object.c index 6ea7a2c6038a0f..b8d7e212fe357c 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1596,9 +1596,7 @@ _PyObject_GetDictPtr(PyObject *obj) PyObject * PyObject_SelfIter(PyObject *obj) { - // Don't need a write barrier since it returns itself. - // _lrc should be increased from the assignment if the iter is in the region - return Py_NewRef(obj); + return PyRegion_NewRef(obj); } /* Helper used when the __next__ method is removed from a type: diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 6887ddfc239cb2..93297d1f95bf32 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -67,8 +67,7 @@ make_range_object(PyTypeObject *type, PyObject *start, return NULL; } } - if(PyRegion_TakeRefs(obj, start, stop, step, length)) - { + if(PyRegion_TakeRefs(obj, start, stop, step, length)) { assert(PyRegion_IsLocal(obj)); // No write barrier bc obj is newly created and being in the local region Py_DECREF(obj); PyRegion_RemoveLocalRef(length); @@ -651,8 +650,7 @@ range_hash(PyObject *op) // len == 0 or not cmp_result = PyObject_Not(r->length); - if (cmp_result == -1) - { + if (cmp_result == -1) { goto end; } From bf2e159432ef0a92cb07731a1b18bf59cf8d9178 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Mon, 23 Mar 2026 15:28:49 +0100 Subject: [PATCH 27/37] update test_set --- Lib/test/test_regions/test_set.py | 75 ++++++++++++++++--------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index 3af6642e2a8fa5..d8e626592aba9e 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -135,41 +135,6 @@ def test_set_move_into_region_fails_if_element_in_another_region(self): self.assertTrue(is_local(ab)) self.assertTrue(is_local(ac)) - def test_iterator(self): - """ - Creating an iterator from a set that borrows from a region should - not increase the LRC, since the iterator itself does not hold - references to the elements (it borrows from the set). - """ - r = Region() - r.word = self.A() - r.word2 = self.A() - r.arr = [r.word, r.word2] - s = set(r.arr) - base_lrc = r._lrc - - r.it = iter(s) - self.assertEqual(r._lrc, base_lrc-1) # s is moved into the region. - it2 = iter(s) - self.assertEqual(r._lrc, base_lrc) - - def test_iterator2(self): - """ - Creating an iterator from a set that borrows from a region should - not increase the LRC, since the iterator itself does not hold - references to the elements (it borrows from the set). - """ - r = Region() - r.word = self.A() - r.word2 = self.A() - r.arr = [r.word, r.word2] - r.s = set(r.arr) - base_lrc = r._lrc - - it = iter(r.s) - self.assertEqual(r._lrc, base_lrc+1) # r.s is moved into the region. - it2 = iter(r.s) - self.assertEqual(r._lrc, base_lrc+2) class TestRegionSetDiscard(unittest.TestCase): @@ -1464,6 +1429,46 @@ def test_next_into_region_transfers_ownership(self): a1 = next(it) # external local ref, LRC + 1 r.a3 = next(it) # moved into region, no external borrow self.assertEqual(r._lrc, base_lrc + 1) + + def test_iterator(self): + """ + Creating an iterator from a set that borrows from a region should + not increase the LRC, since the iterator itself does not hold + references to the elements (it borrows from the set). + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + s = set(r.arr) + base_lrc = r._lrc + + it0 = iter(s) + self.assertEqual(r._lrc, base_lrc) + r.it = iter(s) + self.assertEqual(r._lrc, base_lrc-1+1) # s is moved into the region, but now it0 points to iterator that is moved into the region. + it2 = iter(s) + self.assertEqual(r._lrc, base_lrc+1) + + def test_iterator2(self): + """ + Creating an iterator from a set that borrows from a region should + not increase the LRC, since the iterator itself does not hold + references to the elements (it borrows from the set). + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + r.s = set(r.arr) + base_lrc = r._lrc + + it = iter(r.s) + self.assertEqual(r._lrc, base_lrc+1) # r.s is moved into the region. + it2 = iter(r.s) + self.assertEqual(r._lrc, base_lrc+2) + r.it3 = iter(r.s) + self.assertEqual(r._lrc, base_lrc+2) if __name__ == "__main__": From 01ec042cf2276131d5d477dfae791511ad7a50e8 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Tue, 24 Mar 2026 22:12:19 +0100 Subject: [PATCH 28/37] update set unit test and add enumerate unit test --- Lib/test/test_regions/test_enum.py | 497 +++++++++++++++++++++++++++++ Lib/test/test_regions/test_set.py | 116 +++++++ 2 files changed, 613 insertions(+) create mode 100644 Lib/test/test_regions/test_enum.py diff --git a/Lib/test/test_regions/test_enum.py b/Lib/test/test_regions/test_enum.py new file mode 100644 index 00000000000000..97c830acd1fcc7 --- /dev/null +++ b/Lib/test/test_regions/test_enum.py @@ -0,0 +1,497 @@ +import unittest +from regions import Region, is_local +from immutable import freeze + + +class TestRegionEnumerateBasic(unittest.TestCase): + """Tests for basic enumerate construction and LRC behavior with regions.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_enumerate_from_region_iterator_increases_lrc(self): + """ + Creating an enumerate from a region's iterator should increase + the LRC by 1, since enumerate holds a reference to the iterator. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = enumerate(r.it_arr) + self.assertEqual(r._lrc, base_lrc + 1) + + def test_enumerate_set_to_none_decreases_lrc(self): + """ + Setting the enumerate object to None should release the borrowed + reference to the iterator, bringing LRC back to its pre-enumerate level. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = enumerate(r.it_arr) + self.assertEqual(r._lrc, base_lrc + 1) + + obj = None + self.assertEqual(r._lrc, base_lrc) + + def test_enumerate_from_local_iterator_does_not_change_lrc(self): + """ + Creating an enumerate from a local (non-region) iterator should + not affect the LRC at all. + """ + r = Region() + base_lrc = r._lrc + + local_list = [self.A(), self.A()] + obj = enumerate(local_list) + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionEnumerateNext(unittest.TestCase): + """Tests for next() calls on enumerate objects and their effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_next_on_enumerate_increases_lrc(self): + """ + Calling next() on an enumerate over a region iterator should + yield a (index, element) tuple. The element is a borrowed reference, + so LRC should increase by 1. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + obj = enumerate(r.it_arr) + self.assertEqual(r._lrc, base_lrc + 1) # Because enum object points to r.it_arr in the region + + re1 = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) # Now, re1 points to the first element of r.it_arr, which is r.a, so LRC increases by 1 + + def test_next_on_enumerate_increases_lrc_each_call(self): + """ + Each successive call to next() should increase the LRC by 1, + as each yields a new borrowed element reference. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + obj = enumerate(r.it_arr) + base_lrc = r._lrc + + re1 = next(obj) + self.assertEqual(r._lrc, base_lrc + 1) + + re2 = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + re3 = next(obj) + self.assertEqual(r._lrc, base_lrc + 3) + + re4 = next(obj) + self.assertEqual(r._lrc, base_lrc + 4) + + # @unittest.expectedFailure + def test_next_result_moved_into_region_does_not_increase_lrc(self): + """ + Assigning the result of next() directly into a region should + transfer ownership rather than creating an external borrow, + so LRC should not increase beyond the base. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + obj = enumerate(r.it_arr) + base_lrc = r._lrc + r.re1 = next(obj) + self.assertEqual(r._lrc, base_lrc) + + def test_next_mixed_local_and_region_assignment(self): + """ + A mix of local and region assignments from next() should + reflect only the local (external) borrows in the LRC. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + obj = enumerate(r.it_arr) + base_lrc = r._lrc + + re1 = next(obj) # local borrow: LRC + 1 + r.re2 = next(obj) # moved into region: LRC stays + re3 = next(obj) # local borrow: LRC + 1 + r.re4 = next(obj) # moved into region: LRC stays + self.assertEqual(r._lrc, base_lrc + 2) + + +class TestRegionEnumerateRelease(unittest.TestCase): + """Tests for releasing enumerate results and their effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_setting_next_result_to_none_decreases_lrc(self): + """ + Setting a local next() result to None should release the + borrowed reference and reduce LRC by 1. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + obj = enumerate(r.it_arr) + + re1 = next(obj) + base_lrc = r._lrc + + re1 = None + self.assertEqual(r._lrc, base_lrc - 1) + + def test_setting_all_next_results_to_none_restores_lrc(self): + """ + Releasing all next() results should bring the LRC back + to the pre-next() level, reflecting no more external borrows. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + obj = enumerate(r.it_arr) + base_lrc = r._lrc + + re1 = next(obj) + re2 = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + re1 = None + self.assertEqual(r._lrc, base_lrc + 1) + + re2 = None + self.assertEqual(r._lrc, base_lrc) + + def test_enumerate_set_to_none_after_next_releases_iterator_ref(self): + """ + Releasing the enumerate object itself (not the results) should + drop LRC by 1 for the iterator reference it held. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + + re1 = next(enumerate(r.it_arr)) + base_lrc = r._lrc + + obj = enumerate(r.it_arr) + self.assertEqual(r._lrc, base_lrc + 1) + + obj = None + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionEnumerateMoveIntoRegion(unittest.TestCase): + """Tests for moving enumerate objects and results into regions.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_enumerate_moved_into_region_adjusts_lrc(self): + """ + Moving an enumerate object into a region should transfer ownership + of the iterator reference, so LRC should not increase from the + external variable, but a reference is still held by the region. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = enumerate(r.it_arr) + self.assertEqual(r._lrc, base_lrc + 1) # obj holds external ref + + r.obj = obj + self.assertEqual(r._lrc, base_lrc+1) # obj pointw to the enumerate object inside the region now, so LRC should not increase further + + def test_next_on_region_owned_enumerate_does_not_increase_lrc(self): + """ + Calling next() on an enumerate that is owned by a region (accessed + via region attribute) should not increase the LRC since the element + is moved into the region directly. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + r.obj = enumerate(r.it_arr) + base_lrc = r._lrc + + r.re1 = next(r.obj) + self.assertEqual(r._lrc, base_lrc) + + def test_next_on_region_owned_enumerate_local_assignment_increases_lrc(self): + """ + Calling next() on a region-owned enumerate and assigning to a + local variable should increase LRC by 1 (external borrow). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + r.obj = enumerate(r.it_arr) + base_lrc = r._lrc + + re1 = next(r.obj) + self.assertEqual(r._lrc, base_lrc + 1) + + +class TestRegionEnumerateFullLifecycle(unittest.TestCase): + """End-to-end lifecycle tests matching the example script behavior.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_full_lifecycle_matches_example(self): + """ + Reproduces the exact sequence from the example script: + - create region with a, b + - create arr = [a, b] + - create it_arr = iter(arr) → moves into region + - obj = enumerate(it_arr) → LRC +1 + - re1 = next(obj) → LRC +1 + - r.re2 = next(obj) → LRC +0 (moved into region) + - obj = None → LRC -1 + - re1 = None → LRC -1 + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = enumerate(r.it_arr) + self.assertEqual(r._lrc, base_lrc + 1) + + re1 = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + r.re2 = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + obj = None + self.assertEqual(r._lrc, base_lrc + 1) + + re1 = None + self.assertEqual(r._lrc, base_lrc) + + def test_full_lifecycle_matches_example_2(self): + """ + Reproduces the exact sequence from the example script: + - create region with a, b + - create arr = [a, b] + - create it_arr = iter(arr) → moves into region + - obj = enumerate(it_arr) → LRC +1 + - re1 = next(obj) → LRC +1 + - r.re2 = next(obj) → LRC +0 (moved into region) + - obj = None → LRC -1 + - re1 = None → LRC -1 + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = enumerate(r.it_arr) + self.assertEqual(r._lrc, base_lrc + 1) + + re1 = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + r.re2 = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + re1 = None + self.assertEqual(r._lrc, base_lrc + 1) + + obj = None + self.assertEqual(r._lrc, base_lrc) + + def test_enumerate_result_index_and_value_correct(self): + """ + Ensure the (index, value) tuples from next() contain the correct + index and the actual region element. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + obj = enumerate(r.it_arr) + + idx0, val0 = next(obj) + idx1, val1 = next(obj) + + self.assertEqual(idx0, 0) + self.assertEqual(idx1, 1) + self.assertIs(val0, r.a) + self.assertIs(val1, r.b) + + def test_enumerate_with_start_offset(self): + """ + enumerate(iterable, start=N) should begin indices at N. + LRC behavior is unchanged — it still borrows the iterator. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = enumerate(r.it_arr, start=5) + self.assertEqual(r._lrc, base_lrc + 1) + + idx0, val0 = next(obj) + self.assertEqual(idx0, 5) + self.assertIs(val0, r.a) + + def test_enumerate_exhausted_raises_stop_iteration(self): + """ + Calling next() past the end of the iterable should raise + StopIteration without affecting the LRC. + """ + r = Region() + r.a = self.A() + r.arr = [r.a] + r.it_arr = iter(r.arr) + obj = enumerate(r.it_arr) + + re1 = next(obj) + base_lrc = r._lrc + + with self.assertRaises(StopIteration): + next(obj) + + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionEnumerateTwoRegions(unittest.TestCase): + """Tests for enumerate behavior when elements span multiple regions.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_enumerate_over_local_list_with_mixed_region_elements(self): + """ + Enumerating a local list containing elements from two different + regions should correctly borrow each element independently. + """ + r1 = Region() + r2 = Region() + r1.a = self.A() + r2.b = self.A() + + local_list = [r1.a, r2.b] + it = iter(local_list) + obj = enumerate(it) + + base_r1 = r1._lrc + base_r2 = r2._lrc + + idx0, val0 = next(obj) # borrows r1.a + self.assertEqual(r1._lrc, base_r1 + 1) + self.assertEqual(r2._lrc, base_r2) + + idx1, val1 = next(obj) # borrows r2.b + self.assertEqual(r1._lrc, base_r1 + 1) + self.assertEqual(r2._lrc, base_r2 + 1) + + def test_enumerate_over_local_list_with_mixed_region_elements_2(self): + """ + Enumerating a local list containing elements from two different + regions should correctly borrow each element independently. + """ + r1 = Region() + r2 = Region() + r1.a = self.A() + r1.b = self.A() + + local_list = [r1.a, r1.b] + it = iter(local_list) + with self.assertRaises(Exception): + r2.obj = enumerate(it) + + + def test_enumerate_results_released_independently_per_region(self): + """ + Releasing next() results from two different regions should + decrease each region's LRC independently. + """ + r1 = Region() + r2 = Region() + r1.a = self.A() + r2.b = self.A() + + local_list = [r1.a, r2.b] + it = iter(local_list) + obj = enumerate(it) + + idx0, val0 = next(obj) + idx1, val1 = next(obj) + + base_r1 = r1._lrc + base_r2 = r2._lrc + + val0 = None + self.assertEqual(r1._lrc, base_r1 - 1) + self.assertEqual(r2._lrc, base_r2) + + val1 = None + self.assertEqual(r1._lrc, base_r1 - 1) + self.assertEqual(r2._lrc, base_r2 - 1) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index d8e626592aba9e..2487ca0bf29acf 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -1366,6 +1366,54 @@ def test_iter_creation_does_not_change_lrc(self): it = iter(s) self.assertEqual(r._lrc, base_lrc) + def test_iter_creation_does_not_change_lrc_2(self): + """ + Creating an iterator over a set should not by itself + change the LRC of the region. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + + s = set(r.arr) + base_lrc = r._lrc + + r.it = iter(s) + self.assertEqual(r._lrc, base_lrc-1) + + def test_iter_creation_does_not_change_lrc_3(self): + """ + Creating an iterator over a set should not by itself + change the LRC of the region. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + + r.s = set(r.arr) + base_lrc = r._lrc + + r.it = iter(r.s) + self.assertEqual(r._lrc, base_lrc) + + def test_iter_creation_does_not_change_lrc_4(self): + """ + Creating an iterator over a set should not by itself + change the LRC of the region. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + + r.s = set(r.arr) + base_lrc = r._lrc + + it = iter(r.s) # iterator object points to the object in the region. + self.assertEqual(r._lrc, base_lrc+1) + def test_next_on_iter_increases_lrc(self): """ Calling next() on the iterator yields a borrowed reference, @@ -1394,6 +1442,34 @@ def test_next_on_iter_increases_lrc(self): a4 = next(it) self.assertEqual(r._lrc, base_lrc + 3) + def test_next_on_iter_increases_lrc_2(self): + """ + Calling next() on the iterator yields a borrowed reference, + increasing the LRC by 1. + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.word3 = self.A() + r.word4 = self.A() + r.arr = [r.word, r.word2, r.word3, r.word4] + + s = set(r.arr) + r.it = iter(s) + base_lrc = r._lrc + + r.a1 = next(r.it) + self.assertEqual(r._lrc, base_lrc) + + r.a2 = next(r.it) + self.assertEqual(r._lrc, base_lrc) + + r.a3 = next(r.it) + self.assertEqual(r._lrc, base_lrc) + + r.a4 = next(r.it) + self.assertEqual(r._lrc, base_lrc) + def test_iter_to_none_releases_lrc(self): """ Setting the iterator to None should release the iterator's @@ -1470,6 +1546,46 @@ def test_iterator2(self): r.it3 = iter(r.s) self.assertEqual(r._lrc, base_lrc+2) + def test_iterator_on_iterator(self): + """ + Creating an iterator from a set that borrows from a region should + not increase the LRC, since the iterator itself does not hold + references to the elements (it borrows from the set). + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + s = set(r.arr) + base_lrc = r._lrc + + r.it0 = iter(s) + self.assertEqual(r._lrc, base_lrc-1) + r.it = iter(r.it0) + self.assertEqual(r._lrc, base_lrc-1) + it2 = iter(r.it0) + self.assertEqual(r._lrc, base_lrc-1+1) + + def test_iterator_on_iterator_2(self): + """ + Creating an iterator from a set that borrows from a region should + not increase the LRC, since the iterator itself does not hold + references to the elements (it borrows from the set). + """ + r = Region() + r.word = self.A() + r.word2 = self.A() + r.arr = [r.word, r.word2] + r.s = set(r.arr) + base_lrc = r._lrc + + r.it0 = iter(r.s) + self.assertEqual(r._lrc, base_lrc) + r.it = iter(r.it0) + self.assertEqual(r._lrc, base_lrc) + it2 = iter(r.it0) + self.assertEqual(r._lrc, base_lrc+1) + if __name__ == "__main__": unittest.main() \ No newline at end of file From d84632dece8806364608bb86aa6e0f4585cf2a48 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Wed, 25 Mar 2026 16:08:30 +0100 Subject: [PATCH 29/37] add comment in context.c for better understanding --- Python/context.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Python/context.c b/Python/context.c index 79e5777b72e861..9a1ddcb5ed5e33 100644 --- a/Python/context.c +++ b/Python/context.c @@ -269,7 +269,9 @@ PyContextVar_New(const char *name, PyObject *def) return (PyObject *)var; } - +/* +Argument: the ContextVar Object, fallback value (or NULL), and output parameter for the value (which is set to NULL on error). +*/ int PyContextVar_Get(PyObject *ovar, PyObject *def, PyObject **val) { From 39270ff78dd213ef0aec68f01de8501e066e630c Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Wed, 25 Mar 2026 16:14:20 +0100 Subject: [PATCH 30/37] update set_add_entry_takeref by adding bool to tell if TakeRef is required or not --- Objects/setobject.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Objects/setobject.c b/Objects/setobject.c index b0a88397d48c97..7b351be4a5b9cc 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -137,7 +137,7 @@ set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash) static int set_table_resize(PySetObject *, Py_ssize_t); static int -set_add_entry_takeref(PySetObject *so, PyObject *key, Py_hash_t hash) +set_add_entry_takeref(PySetObject *so, PyObject *key, Py_hash_t hash, bool take_ref) { setentry *table; setentry *freeslot; @@ -200,7 +200,12 @@ set_add_entry_takeref(PySetObject *so, PyObject *key, Py_hash_t hash) if (freeslot == NULL) goto found_unused; FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used + 1); - if(PyRegion_TakeRef(so, key)) return -1; + if (take_ref) { + if (PyRegion_TakeRef(so, key)) { + return -1; + } + } + // if(PyRegion_TakeRef(so, key)) return -1; freeslot->key = key; freeslot->hash = hash; return 0; @@ -208,7 +213,12 @@ set_add_entry_takeref(PySetObject *so, PyObject *key, Py_hash_t hash) found_unused: so->fill++; FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used + 1); - if(PyRegion_TakeRef(so, key)) return -1; + if (take_ref) { + if (PyRegion_TakeRef(so, key)) { + return -1; + } + } + // if(PyRegion_TakeRef(so, key)) return -1; entry->key = key; entry->hash = hash; if ((size_t)so->fill*5 < mask*3) @@ -233,7 +243,7 @@ set_add_entry(PySetObject *so, PyObject *key, Py_hash_t hash) if (PyRegion_AddLocalRef(key)) { return -1; } - return set_add_entry_takeref(so, Py_NewRef(key), hash); + return set_add_entry_takeref(so, Py_NewRef(key), hash, true); } static void @@ -264,7 +274,7 @@ _PySet_AddTakeRef(PySetObject *so, PyObject *key) } // We don't pre-increment here, the caller holds a strong // reference to the object which we are stealing. - return set_add_entry_takeref(so, key, hash); + return set_add_entry_takeref(so, key, hash, false); } /* From 4b6329b92dfbb7c3ad3ee1ea25175bb62fc064c7 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Thu, 26 Mar 2026 21:54:47 +0100 Subject: [PATCH 31/37] update test file --- Lib/test/test_regions/test_enum.py | 15 ++++++++++----- Lib/test/test_regions/test_set.py | 4 ++++ test_code/set_test/setselfiter.py | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 test_code/set_test/setselfiter.py diff --git a/Lib/test/test_regions/test_enum.py b/Lib/test/test_regions/test_enum.py index 97c830acd1fcc7..9256bd8677688e 100644 --- a/Lib/test/test_regions/test_enum.py +++ b/Lib/test/test_regions/test_enum.py @@ -110,7 +110,7 @@ def test_next_on_enumerate_increases_lrc_each_call(self): re4 = next(obj) self.assertEqual(r._lrc, base_lrc + 4) - # @unittest.expectedFailure + @unittest.expectedFailure def test_next_result_moved_into_region_does_not_increase_lrc(self): """ Assigning the result of next() directly into a region should @@ -157,6 +157,7 @@ class A: pass freeze(A()) self.A = A + @unittest.expectedFailure def test_setting_next_result_to_none_decreases_lrc(self): """ Setting a local next() result to None should release the @@ -175,6 +176,7 @@ def test_setting_next_result_to_none_decreases_lrc(self): re1 = None self.assertEqual(r._lrc, base_lrc - 1) + @unittest.expectedFailure def test_setting_all_next_results_to_none_restores_lrc(self): """ Releasing all next() results should bring the LRC back @@ -244,8 +246,9 @@ def test_enumerate_moved_into_region_adjusts_lrc(self): self.assertEqual(r._lrc, base_lrc + 1) # obj holds external ref r.obj = obj - self.assertEqual(r._lrc, base_lrc+1) # obj pointw to the enumerate object inside the region now, so LRC should not increase further + self.assertEqual(r._lrc, base_lrc+1) # obj points to the enumerate object inside the region now, so LRC should not increase further + @unittest.skip("GC ERROR") def test_next_on_region_owned_enumerate_does_not_increase_lrc(self): """ Calling next() on an enumerate that is owned by a region (accessed @@ -263,6 +266,7 @@ def test_next_on_region_owned_enumerate_does_not_increase_lrc(self): r.re1 = next(r.obj) self.assertEqual(r._lrc, base_lrc) + @unittest.skip("GC ERROR") def test_next_on_region_owned_enumerate_local_assignment_increases_lrc(self): """ Calling next() on a region-owned enumerate and assigning to a @@ -322,6 +326,7 @@ def test_full_lifecycle_matches_example(self): re1 = None self.assertEqual(r._lrc, base_lrc) + @unittest.expectedFailure def test_full_lifecycle_matches_example_2(self): """ Reproduces the exact sequence from the example script: @@ -351,7 +356,7 @@ def test_full_lifecycle_matches_example_2(self): self.assertEqual(r._lrc, base_lrc + 2) re1 = None - self.assertEqual(r._lrc, base_lrc + 1) + self.assertEqual(r._lrc, base_lrc + 1) # PROBLEM: LRC does not decrease obj = None self.assertEqual(r._lrc, base_lrc) @@ -440,11 +445,11 @@ def test_enumerate_over_local_list_with_mixed_region_elements(self): base_r1 = r1._lrc base_r2 = r2._lrc - idx0, val0 = next(obj) # borrows r1.a + re1 = next(obj) # borrows r1.a self.assertEqual(r1._lrc, base_r1 + 1) self.assertEqual(r2._lrc, base_r2) - idx1, val1 = next(obj) # borrows r2.b + re2 = next(obj) # borrows r2.b self.assertEqual(r1._lrc, base_r1 + 1) self.assertEqual(r2._lrc, base_r2 + 1) diff --git a/Lib/test/test_regions/test_set.py b/Lib/test/test_regions/test_set.py index 2487ca0bf29acf..31251b6e8133ce 100644 --- a/Lib/test/test_regions/test_set.py +++ b/Lib/test/test_regions/test_set.py @@ -10,6 +10,10 @@ def setUp(self): class A: pass freeze(A()) self.A = A + + def test_initial_lrc(self): + r = Region() + self.assertEqual(r._lrc, 1) def test_set_from_region_array_increases_lrc(self): """ diff --git a/test_code/set_test/setselfiter.py b/test_code/set_test/setselfiter.py new file mode 100644 index 00000000000000..909d27bacc8061 --- /dev/null +++ b/test_code/set_test/setselfiter.py @@ -0,0 +1,18 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +class A: pass +freeze(A()) + +r = Region() +print(f"Region r: {r}") +r.a = A() +r.b = A() +r.arr = [r.a, r.b] +print(f"Region r after creating arr: {r}") +r.s = set(r.arr) +print(f"Region r after creating set: {r}") +it = iter(r.s) +print(f"Region r after creating iterator for set: {r}") +r.it2 = iter(it) +print(f"Region r after creating iterator for iterator: {r}") \ No newline at end of file From eca89fb26fb3e04d3219282364d460c3e6dc3ee6 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Mon, 30 Mar 2026 09:15:35 +0200 Subject: [PATCH 32/37] update enumobject and testcases --- Lib/test/test_regions/test_enum.py | 37 +++++++-- Objects/abstract.c | 1 + Objects/enumobject.c | 118 ++++++++++++++++++++++------- Objects/tupleobject.c | 2 +- test_code/enum_test/create_enum.py | 107 ++++++++++++++++++++++++++ 5 files changed, 231 insertions(+), 34 deletions(-) create mode 100644 test_code/enum_test/create_enum.py diff --git a/Lib/test/test_regions/test_enum.py b/Lib/test/test_regions/test_enum.py index 9256bd8677688e..df1871fbc6965b 100644 --- a/Lib/test/test_regions/test_enum.py +++ b/Lib/test/test_regions/test_enum.py @@ -110,7 +110,7 @@ def test_next_on_enumerate_increases_lrc_each_call(self): re4 = next(obj) self.assertEqual(r._lrc, base_lrc + 4) - @unittest.expectedFailure + # @unittest.expectedFailure def test_next_result_moved_into_region_does_not_increase_lrc(self): """ Assigning the result of next() directly into a region should @@ -125,6 +125,26 @@ def test_next_result_moved_into_region_does_not_increase_lrc(self): obj = enumerate(r.it_arr) base_lrc = r._lrc r.re1 = next(obj) + self.assertEqual(r._lrc, base_lrc+1) + + def test_next_result_moved_into_region_does_not_increase_lrc_2(self): + """ + Assigning the result of next() directly into a region should + transfer ownership rather than creating an external borrow, + so LRC should not increase beyond the base. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + obj = enumerate(r.it_arr) + base_lrc = r._lrc + r.re1 = next(obj) + self.assertEqual(r._lrc, base_lrc+1) + r.re2 = next(obj) self.assertEqual(r._lrc, base_lrc) def test_next_mixed_local_and_region_assignment(self): @@ -157,7 +177,7 @@ class A: pass freeze(A()) self.A = A - @unittest.expectedFailure + # @unittest.expectedFailure def test_setting_next_result_to_none_decreases_lrc(self): """ Setting a local next() result to None should release the @@ -174,9 +194,9 @@ def test_setting_next_result_to_none_decreases_lrc(self): base_lrc = r._lrc re1 = None - self.assertEqual(r._lrc, base_lrc - 1) + self.assertEqual(r._lrc, base_lrc) - @unittest.expectedFailure + # @unittest.expectedFailure def test_setting_all_next_results_to_none_restores_lrc(self): """ Releasing all next() results should bring the LRC back @@ -248,7 +268,7 @@ def test_enumerate_moved_into_region_adjusts_lrc(self): r.obj = obj self.assertEqual(r._lrc, base_lrc+1) # obj points to the enumerate object inside the region now, so LRC should not increase further - @unittest.skip("GC ERROR") + # @unittest.skip("GC ERROR") def test_next_on_region_owned_enumerate_does_not_increase_lrc(self): """ Calling next() on an enumerate that is owned by a region (accessed @@ -266,7 +286,7 @@ def test_next_on_region_owned_enumerate_does_not_increase_lrc(self): r.re1 = next(r.obj) self.assertEqual(r._lrc, base_lrc) - @unittest.skip("GC ERROR") + # @unittest.skip("GC ERROR") def test_next_on_region_owned_enumerate_local_assignment_increases_lrc(self): """ Calling next() on a region-owned enumerate and assigning to a @@ -282,6 +302,9 @@ def test_next_on_region_owned_enumerate_local_assignment_increases_lrc(self): re1 = next(r.obj) self.assertEqual(r._lrc, base_lrc + 1) + re1 = None + self.assertEqual(r._lrc, base_lrc) + r = None class TestRegionEnumerateFullLifecycle(unittest.TestCase): @@ -326,7 +349,7 @@ def test_full_lifecycle_matches_example(self): re1 = None self.assertEqual(r._lrc, base_lrc) - @unittest.expectedFailure + # @unittest.expectedFailure def test_full_lifecycle_matches_example_2(self): """ Reproduces the exact sequence from the example script: diff --git a/Objects/abstract.c b/Objects/abstract.c index 11b3e96a4949aa..b20309e7d6da75 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -1183,6 +1183,7 @@ PyNumber_Add(PyObject *v, PyObject *w) if (result != Py_NotImplemented) { return result; } + assert(PyRegion_IsLocal(result)); Py_DECREF(result); PySequenceMethods *m = Py_TYPE(v)->tp_as_sequence; diff --git a/Objects/enumobject.c b/Objects/enumobject.c index 814ce4f919514b..25d50be6ff44c4 100644 --- a/Objects/enumobject.c +++ b/Objects/enumobject.c @@ -56,6 +56,7 @@ enum_new_impl(PyTypeObject *type, PyObject *iterable, PyObject *start) if (start != NULL) { start = PyNumber_Index(start); if (start == NULL) { + PyRegion_RemoveLocalRef(en); Py_DECREF(en); return NULL; } @@ -64,22 +65,41 @@ enum_new_impl(PyTypeObject *type, PyObject *iterable, PyObject *start) if (en->en_index == -1 && PyErr_Occurred()) { PyErr_Clear(); en->en_index = PY_SSIZE_T_MAX; + if(PyRegion_TakeRef(en, start)) { + PyRegion_RemoveLocalRef(en); + Py_DECREF(en); + return NULL; + } en->en_longindex = start; } else { en->en_longindex = NULL; + PyRegion_RemoveLocalRef(start); Py_DECREF(start); } } else { en->en_index = 0; en->en_longindex = NULL; } - en->en_sit = PyObject_GetIter(iterable); + + // Internally call PyObject_SelfIter having PyRegion_NewRef, so TakeRef should be used + PyObject* itt = PyObject_GetIter(iterable); + if(PyRegion_TakeRef(en, itt)) { + PyRegion_RemoveLocalRef(en); + Py_DECREF(en); + return NULL; + } + en->en_sit = itt; if (en->en_sit == NULL) { + PyRegion_RemoveLocalRef(en); Py_DECREF(en); return NULL; } - en->en_result = PyTuple_Pack(2, Py_None, Py_None); + + PyObject* new_tuple = PyTuple_Pack(2, Py_None, Py_None); + assert(PyRegion_IsLocal(en) && PyRegion_IsLocal(new_tuple)); + en->en_result = new_tuple; if (en->en_result == NULL) { + PyRegion_RemoveLocalRef(en); Py_DECREF(en); return NULL; } @@ -157,6 +177,9 @@ enum_dealloc(PyObject *op) { enumobject *en = _enumobject_CAST(op); PyObject_GC_UnTrack(en); + PyRegion_RemoveRef(en, en->en_sit); + PyRegion_RemoveRef(en, en->en_result); + PyRegion_RemoveRef(en, en->en_longindex); Py_XDECREF(en->en_sit); Py_XDECREF(en->en_result); Py_XDECREF(en->en_longindex); @@ -190,6 +213,7 @@ increment_longindex_lock_held(enumobject *en) if (stepped_up == NULL) { return NULL; } + // ASK FRED: Do I need TakeRef here? en->en_longindex = stepped_up; return next_index; } @@ -207,25 +231,46 @@ enum_next_long(enumobject *en, PyObject* next_item) next_index = increment_longindex_lock_held(en); Py_END_CRITICAL_SECTION(); if (next_index == NULL) { + PyRegion_RemoveLocalRef(next_item); Py_DECREF(next_item); return NULL; } - if (_PyObject_IsUniquelyReferenced(result)) { - Py_INCREF(result); - old_index = PyTuple_GET_ITEM(result, 0); - old_item = PyTuple_GET_ITEM(result, 1); - PyTuple_SET_ITEM(result, 0, next_index); - PyTuple_SET_ITEM(result, 1, next_item); - Py_DECREF(old_index); - Py_DECREF(old_item); - // bpo-42536: The GC may have untracked this result tuple. Since we're - // recycling it, make sure it's tracked again: - _PyTuple_Recycle(result); - return result; + if(result != NULL) { + if (_PyObject_IsUniquelyReferenced(result) && PyRegion_IsLocal(result)) { + if(PyRegion_AddLocalRef(result)){ + PyRegion_RemoveLocalRef(next_item); + PyRegion_RemoveLocalRef(next_index); + Py_DECREF(next_index); + Py_DECREF(next_item); + return NULL; + } + Py_INCREF(result); + old_index = PyTuple_GET_ITEM(result, 0); + old_item = PyTuple_GET_ITEM(result, 1); + PyTuple_SET_ITEM(result, 0, next_index); + PyTuple_SET_ITEM(result, 1, next_item); + PyRegion_RemoveLocalRef(old_index); + PyRegion_RemoveLocalRef(old_item); + Py_DECREF(old_index); + Py_DECREF(old_item); + // bpo-42536: The GC may have untracked this result tuple. Since we're + // recycling it, make sure it's tracked again: + _PyTuple_Recycle(result); + return result; + } + else { + // PyRegion_RemoveRef(en, result); + // Py_DECREF(result); + // en->en_result = NULL; + PyRegion_CLEAR(en, en->en_result); + } } + result = PyTuple_New(2); if (result == NULL) { + PyRegion_RemoveLocalRef(next_index); + PyRegion_RemoveLocalRef(next_item); Py_DECREF(next_index); Py_DECREF(next_item); return NULL; @@ -256,26 +301,47 @@ enum_next(PyObject *op) next_index = PyLong_FromSsize_t(en_index); if (next_index == NULL) { + PyRegion_RemoveLocalRef(next_item); Py_DECREF(next_item); return NULL; } FT_ATOMIC_STORE_SSIZE_RELAXED(en->en_index, en_index + 1); - if (_PyObject_IsUniquelyReferenced(result)) { - Py_INCREF(result); - old_index = PyTuple_GET_ITEM(result, 0); - old_item = PyTuple_GET_ITEM(result, 1); - PyTuple_SET_ITEM(result, 0, next_index); - PyTuple_SET_ITEM(result, 1, next_item); - Py_DECREF(old_index); - Py_DECREF(old_item); - // bpo-42536: The GC may have untracked this result tuple. Since we're - // recycling it, make sure it's tracked again: - _PyTuple_Recycle(result); - return result; + if(result != NULL) { + if (_PyObject_IsUniquelyReferenced(result) && PyRegion_IsLocal(result)) { + if(PyRegion_AddLocalRef(result)){ + PyRegion_RemoveLocalRef(next_item); + PyRegion_RemoveLocalRef(next_index); + Py_DECREF(next_index); + Py_DECREF(next_item); + return NULL; + } + Py_INCREF(result); + old_index = PyTuple_GET_ITEM(result, 0); + old_item = PyTuple_GET_ITEM(result, 1); + PyTuple_SET_ITEM(result, 0, next_index); + PyTuple_SET_ITEM(result, 1, next_item); + PyRegion_RemoveLocalRef(old_index); + PyRegion_RemoveLocalRef(old_item); + Py_DECREF(old_index); + Py_DECREF(old_item); + // bpo-42536: The GC may have untracked this result tuple. Since we're + // recycling it, make sure it's tracked again: + _PyTuple_Recycle(result); + return result; + } + else { + // PyRegion_RemoveRef(en, result); + // Py_DECREF(result); + // en->en_result = NULL; + PyRegion_CLEAR(en, en->en_result); + } } + result = PyTuple_New(2); if (result == NULL) { + PyRegion_RemoveLocalRef(next_index); + PyRegion_RemoveLocalRef(next_item); Py_DECREF(next_index); Py_DECREF(next_item); return NULL; diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 03b3b4c2fd7f36..5b9493a9c5ee12 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -1226,7 +1226,7 @@ tuple_iter(PyObject *seq) static inline int maybe_freelist_push(PyTupleObject *op) { - if (!Py_IS_TYPE(op, &PyTuple_Type)) { + if (!Py_IS_TYPE(op, &PyTuple_Type) || !PyRegion_IsLocal(op)) { return 0; } Py_ssize_t index = Py_SIZE(op) - 1; diff --git a/test_code/enum_test/create_enum.py b/test_code/enum_test/create_enum.py new file mode 100644 index 00000000000000..66c39ba3fb4a2a --- /dev/null +++ b/test_code/enum_test/create_enum.py @@ -0,0 +1,107 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable +from enum import Enum + +class A: pass +freeze(A()) + +r = Region() +input("Press Enter to create objects...") +print(f"Region r: {r}") +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() + +# arr = [r.a, r.b, r.c, r.d, r.e] +# print(f"Region r after creating arr: {r}") +# r.arr = arr +# print(f"Region r after moving arr: {r}") + +#------------------Problem with LRC should not be increases------------------ +# print(f"Region r: {r}") +# r.arr = [r.a, r.b] +# print(f"Region r after creating arr: {r}") +# # input("Press Enter to create enum...") +# r.it_arr = iter(r.arr) +# print(f"Region r after creating iterator: {r}") +# # input("Press Enter to create enum...") +# obj = enumerate(r.it_arr) # LRC +1 since obj points to r.it_arr +# print(f"Region r after creating enum: {r}") +# input("Press Enter to create enum...") +# x = next(obj) +# print(f"Region r after getting next from enum: {r}") +# input("Press Enter to move enum...") +# r.re2 = x +# print(f"Region r after getting next from enum: {r}") +# r.y = next(obj) +# print(f"Region r after getting next from enum: {r}") +# x = None +# print(f"Region r after deleting re1: {r}") +# r.y = None +# print(f"Region r after deleting re2: {r}") +# obj = None +# print(f"Region r after deleting obj: {r}") + +#------------------Problem with LRC not being decreased------------------ +# print(f"Region r: {r}") +# r.arr = [r.a, r.b] +# print(f"Region r after creating arr: {r}") +# # input("Press Enter to create enum...") +# r.it_arr = iter(r.arr) +# print(f"Region r after creating iterator: {r}") +# # input("Press Enter to create enum...") +# obj = enumerate(r.it_arr) # LRC +1 since obj points to r.it_arr +# print(f"Region r after creating enum: {r}") +# # input("Press Enter to create enum...") +# re1 = next(obj) +# print(f"{is_local(obj)}") +# print(f"Region r after getting next from enum: {r}") +# # input("Press Enter to create enum...") +# re2 = next(obj) +# print(f"Region r after getting next from enum: {r}") +# re1 = None # PROBLEM: LRC does not decrease, which should not be since we remove ref. +# print(f"Region r after deleting re1: {r}") +# re2 = None +# print(f"Region r after deleting re2: {r}") +# obj = None +# print(f"Region r after deleting obj: {r}") + +#------------------Problem with GC------------------ +# print(f"Region r: {r}") +# r.arr = [r.a, r.b] +# print(f"Region r after creating arr: {r}") +# # input("Press Enter to create enum...") +# r.it_arr = iter(r.arr) +# print(f"Region r after creating iterator: {r}") +# input("Press Enter to create enum...") +# r.obj = enumerate(r.it_arr) # LRC +1 since obj points to r.it_arr +# print(f"Region r after creating enum: {r}") +# input("Press Enter to create enum...") +# re1 = next(r.obj) +# print(f"{is_local(r.obj)}") +# print(f"Region r after getting next from enum: {r}") +# input("Press Enter to create enum...") +# r.re2 = next(r.obj) +# print(f"Region r after getting next from enum: {r}") + + +print(f"Region r: {r}") +r.arr = [r.a, r.b, r.c, r.d, r.e] +print(f"Region r after creating arr: {r}") +# input("Press Enter to create enum...") +r.it_arr = iter(r.arr) +print(f"Region r after creating iterator: {r}") +# input("Press Enter to create enum...") +obj = enumerate(r.it_arr) # LRC +1 since obj points to r.it_arr +print(f"Region r after creating enum: {r}") +input("Press Enter to create enum...") +re = next(obj) +print(f"Region r after getting next from enum: {r}") +r.re2 = next(obj) +print(f"Region r after getting next from enum: {r}") +re3 = next(obj) +print(f"Region r after getting next from enum: {r}") +r.re4 = next(obj) +print(f"Region r after getting next from enum: {r}") \ No newline at end of file From 24940947e8ec3ac9854a64dba519a25d3d6a0ead Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Tue, 31 Mar 2026 14:49:16 +0200 Subject: [PATCH 33/37] update itertools batched and pairwise (need to be refined, but I think it is correct as of now) --- Modules/clinic/itertoolsmodule.c.h | 1 + Modules/itertoolsmodule.c | 113 +++++++++++++++--- test_code/enum_test/create_enum.py | 34 +++--- test_code/itertool_test/itertool_batch1.py | 79 ++++++++++++ test_code/itertool_test/itertool_pairwise1.py | 85 +++++++++++++ 5 files changed, 280 insertions(+), 32 deletions(-) create mode 100644 test_code/itertool_test/itertool_batch1.py create mode 100644 test_code/itertool_test/itertool_pairwise1.py diff --git a/Modules/clinic/itertoolsmodule.c.h b/Modules/clinic/itertoolsmodule.c.h index 49816bfcb42fec..9e76ed6020e8b8 100644 --- a/Modules/clinic/itertoolsmodule.c.h +++ b/Modules/clinic/itertoolsmodule.c.h @@ -84,6 +84,7 @@ batched_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) PyObject *iobj = _PyNumber_Index(fastargs[1]); if (iobj != NULL) { ival = PyLong_AsSsize_t(iobj); + PyRegion_RemoveLocalRef(iobj); Py_DECREF(iobj); } if (ival == -1 && PyErr_Occurred()) { diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 60ef6f9ff4cd98..80d16b2c03db70 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -157,10 +157,18 @@ batched_new_impl(PyTypeObject *type, PyObject *iterable, Py_ssize_t n, /* create batchedobject structure */ bo = (batchedobject *)type->tp_alloc(type, 0); if (bo == NULL) { + PyRegion_RemoveLocalRef(it); Py_DECREF(it); return NULL; } bo->batch_size = n; + if(PyRegion_TakeRef(bo, it)) { + PyRegion_RemoveLocalRef(bo); + PyRegion_RemoveLocalRef(it); + Py_DECREF(bo); + Py_DECREF(it); + return NULL; + } bo->it = it; bo->strict = (bool) strict; return (PyObject *)bo; @@ -172,8 +180,10 @@ batched_dealloc(PyObject *op) batchedobject *bo = batchedobject_CAST(op); PyTypeObject *tp = Py_TYPE(bo); PyObject_GC_UnTrack(bo); + PyRegion_RemoveRef(bo, bo->it); Py_XDECREF(bo->it); tp->tp_free(bo); + PyRegion_RemoveLocalRef(tp); Py_DECREF(tp); } @@ -220,8 +230,9 @@ batched_next(PyObject *op) /* Input raised an exception other than StopIteration */ FT_ATOMIC_STORE_SSIZE_RELAXED(bo->batch_size, -1); #ifndef Py_GIL_DISABLED - Py_CLEAR(bo->it); + PyRegion_CLEAR(bo, bo->it); #endif + PyRegion_RemoveLocalRef(result); Py_DECREF(result); return NULL; } @@ -230,16 +241,18 @@ batched_next(PyObject *op) if (i == 0) { FT_ATOMIC_STORE_SSIZE_RELAXED(bo->batch_size, -1); #ifndef Py_GIL_DISABLED - Py_CLEAR(bo->it); + PyRegion_CLEAR(bo, bo->it); #endif + PyRegion_RemoveLocalRef(result); Py_DECREF(result); return NULL; } if (bo->strict) { FT_ATOMIC_STORE_SSIZE_RELAXED(bo->batch_size, -1); #ifndef Py_GIL_DISABLED - Py_CLEAR(bo->it); + PyRegion_CLEAR(bo, bo->it); #endif + PyRegion_RemoveLocalRef(result); Py_DECREF(result); PyErr_SetString(PyExc_ValueError, "batched(): incomplete batch"); return NULL; @@ -305,6 +318,14 @@ pairwise_new_impl(PyTypeObject *type, PyObject *iterable) } po = (pairwiseobject *)type->tp_alloc(type, 0); if (po == NULL) { + PyRegion_RemoveLocalRef(it); + Py_DECREF(it); + return NULL; + } + if(PyRegion_TakeRef(po, it)) { + PyRegion_RemoveLocalRef(po); + PyRegion_RemoveLocalRef(it); + Py_DECREF(po); Py_DECREF(it); return NULL; } @@ -312,6 +333,7 @@ pairwise_new_impl(PyTypeObject *type, PyObject *iterable) po->old = NULL; po->result = PyTuple_Pack(2, Py_None, Py_None); if (po->result == NULL) { + PyRegion_RemoveLocalRef(po); Py_DECREF(po); return NULL; } @@ -324,10 +346,14 @@ pairwise_dealloc(PyObject *op) pairwiseobject *po = pairwiseobject_CAST(op); PyTypeObject *tp = Py_TYPE(po); PyObject_GC_UnTrack(po); + PyRegion_RemoveRef(po, po->it); + PyRegion_RemoveRef(po, po->old); + PyRegion_RemoveRef(po, po->result); Py_XDECREF(po->it); Py_XDECREF(po->old); Py_XDECREF(po->result); tp->tp_free(po); + PyRegion_RemoveLocalRef(tp); Py_DECREF(tp); } @@ -355,34 +381,57 @@ pairwise_next(PyObject *op) } if (old == NULL) { old = (*Py_TYPE(it)->tp_iternext)(it); - Py_XSETREF(po->old, old); + PyRegion_XSETREF(po, po->old, old); + // Py_XSETREF(po->old, old); if (old == NULL) { - Py_CLEAR(po->it); + PyRegion_CLEAR(po, po->it); return NULL; } it = po->it; if (it == NULL) { - Py_CLEAR(po->old); + PyRegion_CLEAR(po, po->old); return NULL; } } + PyRegion_AddLocalRef(old); Py_INCREF(old); new = (*Py_TYPE(it)->tp_iternext)(it); if (new == NULL) { - Py_CLEAR(po->it); - Py_CLEAR(po->old); + PyRegion_CLEAR(po, po->it); + PyRegion_CLEAR(po, po->old); + PyRegion_RemoveLocalRef(old); Py_DECREF(old); return NULL; } result = po->result; - if (Py_REFCNT(result) == 1) { + if (Py_REFCNT(result) == 1 && PyRegion_IsLocal(result)) { + if(PyRegion_AddLocalRef(result)) { + PyRegion_RemoveLocalRef(old); + PyRegion_RemoveLocalRef(new); + Py_DECREF(old); + Py_DECREF(new); + return NULL; + } Py_INCREF(result); PyObject *last_old = PyTuple_GET_ITEM(result, 0); PyObject *last_new = PyTuple_GET_ITEM(result, 1); - PyTuple_SET_ITEM(result, 0, Py_NewRef(old)); - PyTuple_SET_ITEM(result, 1, Py_NewRef(new)); + PyObject *old_region = PyRegion_NewRef(old); + PyObject *new_region = PyRegion_NewRef(new); + if(PyRegion_TakeRefs(result, old_region, new_region)) { + PyRegion_RemoveLocalRef(old_region); + PyRegion_RemoveLocalRef(new_region); + Py_DECREF(old_region); + Py_DECREF(new_region); + PyRegion_RemoveLocalRef(result); + Py_DECREF(result); + return NULL; + } + PyTuple_SET_ITEM(result, 0, old_region); + PyTuple_SET_ITEM(result, 1, new_region); + PyRegion_RemoveLocalRef(last_old); Py_DECREF(last_old); + PyRegion_RemoveLocalRef(last_new); Py_DECREF(last_new); // bpo-42536: The GC may have untracked this result tuple. Since we're // recycling it, make sure it's tracked again: @@ -391,12 +440,24 @@ pairwise_next(PyObject *op) else { result = PyTuple_New(2); if (result != NULL) { - PyTuple_SET_ITEM(result, 0, Py_NewRef(old)); - PyTuple_SET_ITEM(result, 1, Py_NewRef(new)); + PyObject *old_region = PyRegion_NewRef(old); + PyObject *new_region = PyRegion_NewRef(new); + if(PyRegion_TakeRefs(result, old_region, new_region)) { + PyRegion_RemoveLocalRef(old_region); + PyRegion_RemoveLocalRef(new_region); + Py_DECREF(old_region); + Py_DECREF(new_region); + PyRegion_RemoveLocalRef(result); + Py_DECREF(result); + return NULL; + } + PyTuple_SET_ITEM(result, 0, old_region); + PyTuple_SET_ITEM(result, 1, new_region); } } - - Py_XSETREF(po->old, new); + PyRegion_XSETREF(po, po->old, new); + // Py_XSETREF(po->old, new); + PyRegion_RemoveLocalRef(old); Py_DECREF(old); return result; } @@ -4004,6 +4065,28 @@ static int itertoolsmodule_clear(PyObject *mod) { itertools_state *state = get_module_state(mod); + PyRegion_RemoveLocalRef(state->accumulate_type); + PyRegion_RemoveLocalRef(state->batched_type); + PyRegion_RemoveLocalRef(state->chain_type); + PyRegion_RemoveLocalRef(state->combinations_type); + PyRegion_RemoveLocalRef(state->compress_type); + PyRegion_RemoveLocalRef(state->count_type); + PyRegion_RemoveLocalRef(state->cwr_type); + PyRegion_RemoveLocalRef(state->cycle_type); + PyRegion_RemoveLocalRef(state->dropwhile_type); + PyRegion_RemoveLocalRef(state->filterfalse_type); + PyRegion_RemoveLocalRef(state->groupby_type); + PyRegion_RemoveLocalRef(state->_grouper_type); + PyRegion_RemoveLocalRef(state->islice_type); + PyRegion_RemoveLocalRef(state->pairwise_type); + PyRegion_RemoveLocalRef(state->permutations_type); + PyRegion_RemoveLocalRef(state->product_type); + PyRegion_RemoveLocalRef(state->repeat_type); + PyRegion_RemoveLocalRef(state->starmap_type); + PyRegion_RemoveLocalRef(state->takewhile_type); + PyRegion_RemoveLocalRef(state->tee_type); + PyRegion_RemoveLocalRef(state->teedataobject_type); + PyRegion_RemoveLocalRef(state->ziplongest_type); Py_CLEAR(state->accumulate_type); Py_CLEAR(state->batched_type); Py_CLEAR(state->chain_type); diff --git a/test_code/enum_test/create_enum.py b/test_code/enum_test/create_enum.py index 66c39ba3fb4a2a..e1cce5fcf19dbc 100644 --- a/test_code/enum_test/create_enum.py +++ b/test_code/enum_test/create_enum.py @@ -87,21 +87,21 @@ class A: pass # print(f"Region r after getting next from enum: {r}") -print(f"Region r: {r}") -r.arr = [r.a, r.b, r.c, r.d, r.e] -print(f"Region r after creating arr: {r}") -# input("Press Enter to create enum...") -r.it_arr = iter(r.arr) -print(f"Region r after creating iterator: {r}") +# print(f"Region r: {r}") +# r.arr = [r.a, r.b, r.c, r.d, r.e] +# print(f"Region r after creating arr: {r}") +# # input("Press Enter to create enum...") +# r.it_arr = iter(r.arr) +# print(f"Region r after creating iterator: {r}") +# # input("Press Enter to create enum...") +# obj = enumerate(r.it_arr) # LRC +1 since obj points to r.it_arr +# print(f"Region r after creating enum: {r}") # input("Press Enter to create enum...") -obj = enumerate(r.it_arr) # LRC +1 since obj points to r.it_arr -print(f"Region r after creating enum: {r}") -input("Press Enter to create enum...") -re = next(obj) -print(f"Region r after getting next from enum: {r}") -r.re2 = next(obj) -print(f"Region r after getting next from enum: {r}") -re3 = next(obj) -print(f"Region r after getting next from enum: {r}") -r.re4 = next(obj) -print(f"Region r after getting next from enum: {r}") \ No newline at end of file +# re = next(obj) +# print(f"Region r after getting next from enum: {r}") +# r.re2 = next(obj) +# print(f"Region r after getting next from enum: {r}") +# re3 = next(obj) +# print(f"Region r after getting next from enum: {r}") +# r.re4 = next(obj) +# print(f"Region r after getting next from enum: {r}") \ No newline at end of file diff --git a/test_code/itertool_test/itertool_batch1.py b/test_code/itertool_test/itertool_batch1.py new file mode 100644 index 00000000000000..d1da440b32d893 --- /dev/null +++ b/test_code/itertool_test/itertool_batch1.py @@ -0,0 +1,79 @@ +from itertools import batched +from regions import Region, is_local +from immutable import freeze, register_freezable + +class A: pass +freeze(A()) +freeze(batched) +r = Region() +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() + +# r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] +# print(f"Region r after creating arr: {r}") +# r.it_arr = iter(r.arr) +# print(f"Region r after creating iterator: {r}") +# obj = batched(r.it_arr, 3) +# print(f"Region r after creating batched iterator: {r}") +# x = next(obj) +# print(f"Region r after getting next from batched iterator: {r}") +# print(f"x: {x}") +# y = next(obj) +# print(f"Region r after getting next from batched iterator: {r}") +# print(f"y: {y}") +# x = None +# print(f"Region r after deleting x: {r}") +# y = None +# print(f"Region r after deleting y: {r}") +# obj = None +# print(f"Region r after deleting obj: {r}") + +# r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] +# print(f"Region r after creating arr: {r}") +# r.it_arr = iter(r.arr) +# print(f"Region r after creating iterator: {r}") +# input("Press Enter to create batched iterator...") +# obj = batched(r.it_arr, 3) +# print(f"Region r after creating batched iterator: {r}") +# input("Press Enter to assign batched iterator to r.obj...") +# r.obj = obj +# print(f"Region r after assigning batched iterator to r.obj: {r}") +# r.x = next(r.obj) +# print(f"Region r after getting next from batched iterator: {r}") +# print(f"x: {r.x}") +# y = next(r.obj) +# print(f"Region r after getting next from batched iterator: {r}") +# print(f"y: {y}") +# r.x = None +# print(f"Region r after deleting x: {r}") +# y = None +# print(f"Region r after deleting y: {r}") +# obj = None +# print(f"Region r after deleting obj: {r}") + +r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] +print(f"Region r after creating arr: {r}") +r.it_arr = iter(r.arr) +print(f"Region r after creating iterator: {r}") +input("Press Enter to create batched iterator...") +obj = batched(r.it_arr, 3) +print(f"Region r after creating batched iterator: {r}") +input("Press Enter to assign batched iterator to r.obj...") +r.obj = obj +print(f"Region r after assigning batched iterator to r.obj: {r}") +x = next(r.obj) +print(f"Region r after getting next from batched iterator: {r}") +print(f"x: {x}") +r.y = next(r.obj) +print(f"Region r after getting next from batched iterator: {r}") +print(f"y: {r.y}") +x = None +print(f"Region r after deleting x: {r}") +r.y = None +print(f"Region r after deleting y: {r}") +obj = None +print(f"Region r after deleting obj: {r}") \ No newline at end of file diff --git a/test_code/itertool_test/itertool_pairwise1.py b/test_code/itertool_test/itertool_pairwise1.py new file mode 100644 index 00000000000000..f883e17651bd0a --- /dev/null +++ b/test_code/itertool_test/itertool_pairwise1.py @@ -0,0 +1,85 @@ +from itertools import pairwise +from regions import Region, is_local +from immutable import freeze, register_freezable + +class A: pass +freeze(A()) +# freeze(batched) +r = Region() +r.a = A() +r.b = A() +r.c = A() +r.d = A() +r.e = A() +r.f = A() + +r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] +print(f"Region r after creating arr: {r}") +r.it_arr = iter(r.arr) +print(f"Region r after creating iterator: {r}") +obj = pairwise(r.it_arr) +print(f"Region r after creating pairwise iterator: {r}") +input("Press Enter to assign pairwise iterator to r.obj...") +x = next(obj) +print(f"Region r after getting next from pairwise iterator: {r}") +print(f"x: {x}") +y = next(obj) +print(f"Region r after getting next from pairwise iterator: {r}") +print(f"y: {y}") +r.z = next(obj) +print(f"Region r after getting next from pairwise iterator: {r}") +print(f"z: {r.z}") +x = None +print(f"Region r after deleting x: {r}") +y = None +print(f"Region r after deleting y: {r}") +r.z = None +print(f"Region r after deleting z: {r}") +obj = None +print(f"Region r after deleting obj: {r}") + +# r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] +# print(f"Region r after creating arr: {r}") +# r.it_arr = iter(r.arr) +# print(f"Region r after creating iterator: {r}") +# input("Press Enter to create pairwise iterator...") +# obj = pairwise(r.it_arr) +# print(f"Region r after creating pairwise iterator: {r}") +# input("Press Enter to assign pairwise iterator to r.obj...") +# r.obj = obj +# print(f"Region r after assigning pairwise iterator to r.obj: {r}") +# r.x = next(r.obj) +# print(f"Region r after getting next from pairwise iterator: {r}") +# print(f"x: {r.x}") +# y = next(r.obj) +# print(f"Region r after getting next from pairwise iterator: {r}") +# print(f"y: {y}") +# r.x = None +# print(f"Region r after deleting x: {r}") +# y = None +# print(f"Region r after deleting y: {r}") +# obj = None +# print(f"Region r after deleting obj: {r}") + +# r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] +# print(f"Region r after creating arr: {r}") +# r.it_arr = iter(r.arr) +# print(f"Region r after creating iterator: {r}") +# input("Press Enter to create pairwise iterator...") +# obj = pairwise(r.it_arr) +# print(f"Region r after creating pairwise iterator: {r}") +# input("Press Enter to assign pairwise iterator to r.obj...") +# r.obj = obj +# print(f"Region r after assigning pairwise iterator to r.obj: {r}") +# x = next(r.obj) +# print(f"Region r after getting next from pairwise iterator: {r}") +# print(f"x: {x}") +# r.y = next(r.obj) +# print(f"Region r after getting next from pairwise iterator: {r}") +# print(f"y: {r.y}") +# x = None +# print(f"Region r after deleting x: {r}") +# r.y = None +# print(f"Region r after deleting y: {r}") +# obj = None +# print(f"Region r after deleting obj: {r}") \ No newline at end of file From 3d786ecd0d3279ed8a7b72c244f7cec2f570d3ac Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Wed, 1 Apr 2026 10:59:00 +0200 Subject: [PATCH 34/37] update testcases and itertools.pairwise --- Lib/test/test_regions/test_itertools.py | 831 ++++++++++++++++++ Modules/itertoolsmodule.c | 92 +- test_code/enum_test/create_enum.py | 56 +- test_code/itertool_test/itertool_pairwise1.py | 97 +- test_code/report_code/onlygil.py | 33 + 5 files changed, 996 insertions(+), 113 deletions(-) create mode 100644 Lib/test/test_regions/test_itertools.py create mode 100644 test_code/report_code/onlygil.py diff --git a/Lib/test/test_regions/test_itertools.py b/Lib/test/test_regions/test_itertools.py new file mode 100644 index 00000000000000..e31b6d9f698017 --- /dev/null +++ b/Lib/test/test_regions/test_itertools.py @@ -0,0 +1,831 @@ +import unittest +from itertools import batched, pairwise +from regions import Region, is_local +from immutable import freeze, register_freezable + + +freeze(batched) +freeze(pairwise) + + +class TestRegionBatchedBasic(unittest.TestCase): + """Tests for basic batched iterator construction and LRC behavior with regions.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_initial_lrc(self): + r = Region() + self.assertEqual(r._lrc, 1) + + def test_batched_from_local_iter_does_not_change_lrc(self): + """ + Creating a batched iterator from a local iterator over a region array + should not change the LRC — the batched object itself is local and + holds no references yet. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + + it = iter(r.arr) + base_lrc = r._lrc + + obj = batched(it, 2) + self.assertEqual(r._lrc, base_lrc) + self.assertTrue(is_local(obj)) + + def test_batched_moved_into_region_adjusts_lrc(self): + """ + Moving a batched iterator into a region should adjust the LRC: + the region now owns the batched object, so the external reference + is no longer borrowed. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = batched(r.it_arr, 2) + self.assertEqual(r._lrc, base_lrc + 1) # obj borrows r.it_arr + + r.obj = obj + self.assertEqual(r._lrc, base_lrc+1) # obj points to the batched object. + + +class TestRegionBatchedNext(unittest.TestCase): + """Tests for next() on batched iterators and their effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_next_on_local_batched_increases_lrc(self): + """ + Calling next() on a local batched iterator yields a tuple of + borrowed references, increasing the LRC by the batch size. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] + r.it_arr = iter(r.arr) + + obj = batched(r.it_arr, 3) + base_lrc = r._lrc + + x = next(obj) + # x is a tuple of 3 borrowed refs (a, b, c) + self.assertEqual(r._lrc, base_lrc + 3) + + def test_next_twice_on_local_batched_increases_lrc_cumulatively(self): + """ + Each call to next() borrows another batch of elements, so the LRC + should increase by batch_size for each call. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] + r.it_arr = iter(r.arr) + + obj = batched(r.it_arr, 3) + base_lrc = r._lrc + + x = next(obj) + self.assertEqual(r._lrc, base_lrc + 3) + + y = next(obj) + self.assertEqual(r._lrc, base_lrc + 6) + + def test_next_result_released_decreases_lrc(self): + """ + Releasing the tuple returned by next() should release all borrowed + references it holds, bringing the LRC back down. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] + r.it_arr = iter(r.arr) + + obj = batched(r.it_arr, 3) + base_lrc = r._lrc + + x = next(obj) + self.assertEqual(r._lrc, base_lrc + 3) + + y = next(obj) + self.assertEqual(r._lrc, base_lrc + 6) + + x = None + self.assertEqual(r._lrc, base_lrc + 3) + + y = None + self.assertEqual(r._lrc, base_lrc) + + def test_next_result_moved_into_region_adjusts_lrc(self): + """ + Assigning the result of next() directly into the region transfers + ownership of the tuple and its elements, so the LRC should not + increase by the full batch size for a region-owned result. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] + r.it_arr = iter(r.arr) + + r.obj = batched(r.it_arr, 3) + base_lrc = r._lrc + + x = next(r.obj) # external local ref to tuple of 3 elements + self.assertEqual(r._lrc, base_lrc + 3) + + r.y = next(r.obj) # moved into region, no external borrow + self.assertEqual(r._lrc, base_lrc + 3) # only x's 3 refs still borrowed + + +class TestRegionBatchedRelease(unittest.TestCase): + """Tests for releasing batched iterators and their effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_batched_set_to_none_releases_lrc(self): + """ + Setting the batched iterator to None before consuming it should + release any references it holds (none, if next() was never called). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = batched(r.it_arr, 2) + obj = None + self.assertEqual(r._lrc, base_lrc) + + def test_partial_consumption_then_release(self): + """ + Releasing a partially consumed batched iterator (after one next()) + should only drop the reference to the batched object itself, not + any already-yielded tuples. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + + obj = batched(r.it_arr, 2) + x = next(obj) # borrows a, b + base_lrc = r._lrc + + obj = None # release the iterator; x still holds a, b + self.assertEqual(r._lrc, base_lrc-1) + + x = None # now release the tuple + self.assertEqual(r._lrc, base_lrc-1-2) # -1 for obj, -2 for a,b + + def test_region_owned_batched_set_to_none(self): + """ + Releasing a region-owned batched iterator by setting it to None + should clean up any internal state without external LRC leaks. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] + r.it_arr = iter(r.arr) + + r.obj = batched(r.it_arr, 3) + x = next(r.obj) # borrows 3 elements externally + r.y = next(r.obj) # owned by region + + base_lrc = r._lrc + + x = None + self.assertEqual(r._lrc, base_lrc - 3) + + r.y = None + self.assertEqual(r._lrc, base_lrc - 3) # r.y was owned, no external borrow change + + +class TestRegionBatchedUnevenBatch(unittest.TestCase): + """Tests for batched iterators where the last batch is smaller than batch_size.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_last_batch_smaller(self): + """ + When the number of elements is not evenly divisible by batch_size, + the last batch should contain fewer elements, and the LRC increase + should reflect the actual number of elements in that batch. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.arr = [r.a, r.b, r.c, r.d, r.e] + r.it_arr = iter(r.arr) + + obj = batched(r.it_arr, 3) + base_lrc = r._lrc + + x = next(obj) # full batch: a, b, c + self.assertEqual(r._lrc, base_lrc + 3) + + y = next(obj) # partial batch: d, e + self.assertEqual(r._lrc, base_lrc + 3 + 2) + + x = None + self.assertEqual(r._lrc, base_lrc + 2) + + y = None + self.assertEqual(r._lrc, base_lrc) + + def test_single_element_batch(self): + """ + A batch size of 1 should yield one element at a time, increasing + the LRC by 1 per next() call. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + + obj = batched(r.it_arr, 1) + base_lrc = r._lrc + + x = next(obj) + self.assertEqual(r._lrc, base_lrc + 1) + + y = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + x = None + y = None + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionBatchedFromLocalIterator(unittest.TestCase): + """Tests for batched over a local (non-region) iterator that yields region objects.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_batched_from_local_list_of_region_objects(self): + """ + Creating a batched iterator from a plain local list containing + region objects should borrow those objects when next() is called. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + arr = [r.a, r.b, r.c, r.d] # local list, not in region + + obj = batched(iter(arr), 2) + base_lrc = r._lrc + + x = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + y = next(obj) + self.assertEqual(r._lrc, base_lrc + 4) + + x = None + y = None + self.assertEqual(r._lrc, base_lrc) + + def test_batched_next_into_region_from_local_list(self): + """ + Assigning the result of next() into the region when iterating + a local list of region objects should transfer ownership. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + arr = [r.a, r.b, r.c, r.d] + + obj = batched(iter(arr), 2) + base_lrc = r._lrc + + r.x = next(obj) # owned by region + self.assertEqual(r._lrc, base_lrc) + + y = next(obj) # external borrow + self.assertEqual(r._lrc, base_lrc + 2) + + y = None + self.assertEqual(r._lrc, base_lrc) + + +class TestRegionBatchedIsLocal(unittest.TestCase): + """Tests that batched iterators and their results have correct locality.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_local_batched_is_local(self): + """ + A batched iterator created outside of a region should be local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + it = iter(r.arr) + + obj = batched(it, 2) + self.assertTrue(is_local(obj)) + + def test_next_result_is_local(self): + """ + The tuple returned by next() on a local batched iterator should be local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + + obj = batched(r.it_arr, 2) + x = next(obj) + self.assertTrue(is_local(x)) + + def test_region_owned_batched_is_not_local(self): + """ + A batched iterator moved into a region should no longer be local. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + r.it_arr = iter(r.arr) + + r.obj = batched(r.it_arr, 2) + self.assertFalse(is_local(r.obj)) + +class TestRegionPairwiseBasic(unittest.TestCase): + """Tests for basic pairwise iterator construction and LRC behavior.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_pairwise_from_region_iterator_does_not_change_lrc(self): + """ + Creating a pairwise iterator from a region-owned iterator should + not change the LRC — the pairwise object borrows the iterator + but holds no element references yet. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = pairwise(r.it_arr) + # pairwise borrows r.it_arr externally + self.assertEqual(r._lrc, base_lrc + 1) + self.assertTrue(is_local(obj)) + + def test_pairwise_from_local_iterator_does_not_change_lrc(self): + """ + Creating a pairwise iterator from a local iterator (not region-owned) + should not change the LRC. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + it = iter(r.arr) + base_lrc = r._lrc + + obj = pairwise(it) + self.assertEqual(r._lrc, base_lrc) + self.assertTrue(is_local(obj)) + + def test_pairwise_moved_into_region_adjusts_lrc(self): + """ + Moving a pairwise iterator into a region should transfer ownership, + removing the external borrow on the underlying iterator. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = pairwise(r.it_arr) + self.assertEqual(r._lrc, base_lrc + 1) # pairwise points to r.it_arr + + r.obj = obj + self.assertEqual(r._lrc, base_lrc + 1) # obj points to pairwise + + +class TestRegionPairwiseNextLocal(unittest.TestCase): + """ + Tests for next() on a local pairwise iterator over a region-owned iterator. + + pairwise internally caches the right element of the last yielded pair as `old`. + On the first next(): + - yields (left, right): LRC +2 for the tuple elements, +1 for obj's internal + ref to `old` (right) = +3 total. + On subsequent next() calls (when `old` is already cached): + - the new left == previous right, already counted via `old`, + so: +2 for the new tuple, -1 for releasing old `old` ref = net +1... + but if previous tuple is still alive (sharing the right element), + the ref to `old` is not a new borrow = +2 net. + """ + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_first_next_increases_lrc_by_3(self): + """ + The first next() yields (a, b) and caches b as `old`. + LRC increases by 3: +2 for the tuple (a, b), +1 for internal `old` ref to b. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + base_lrc = r._lrc + + x = next(obj) + self.assertEqual(r._lrc, base_lrc + 3) # tuple(a,b) + internal old->b + + def test_release_first_tuple_decreases_lrc_by_2(self): + """ + Releasing the first tuple (a, b) drops 2 refs. + The internal `old` ref to b is still held by pairwise, so LRC drops by 2. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + base_lrc = r._lrc + + x = next(obj) + self.assertEqual(r._lrc, base_lrc + 3) + + x = None # releases (a, b) but old still holds b + self.assertEqual(r._lrc, base_lrc + 3) # The tuple that points to two elements in the region is not deallocated because obj->result still points to. + + def test_second_next_after_release_does_not_increase_lrc(self): + """ + After releasing x=(a,b) and calling next() again to get (b,c): + b is already borrowed via `old`, so only c is a new borrow. + But old is updated to c, releasing the old->b ref. + Net change from the second next(): 0 (new tuple borrows b,c: +2, + old releases b: -1, old takes c: already counted in tuple = net +2 -1 = +1, + but since we re-use old's slot for the right element: net 0 from base+1). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + base_lrc = r._lrc + + x = next(obj) # LRC +3 + x = None # LRC -0 + x = next(obj) # LRC +0 since the first next(obj) has set up the internal state of pairwise to reuse the old ref for the right element of the new tuple. + + self.assertEqual(r._lrc, base_lrc + 3) + + def test_second_next_while_first_tuple_alive(self): + """ + Calling next() a second time while first tuple x=(a,b) is still alive. + Second tuple y=(b,c): b is shared between x and old, c is new. + old moves from b to c: -1 for old->b, +1 for old->c, +1 for new c in y's tuple. + But b in y's tuple was already in old: net +2 for y (b already counted), -1 for old release of b, +1 new old->c. + Total from base_lrc (after first next at base+3): +2 for y = base+3+2 = base+5? + Actually: y=(b,c) borrows b (was in old, now also in tuple +1) and c (+1), old->c replaces old->b (net 0 for old). + So +2 for tuple y's new borrows. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + base_lrc = r._lrc # Initial LRC includes the borrow ref from pairwise to r.it_arr + + x = next(obj) # (a,b), old=b → base+3 + self.assertEqual(r._lrc, base_lrc + 3) + + y = next(obj) # (b,c), old=c → +2 for (b,c) in y, old moves b→c (net 0) + self.assertEqual(r._lrc, base_lrc + 5) + + x = None # releases (a, b): -2 + self.assertEqual(r._lrc, base_lrc + 3) + + y = None # releases (b, c): -2 + self.assertEqual(r._lrc, base_lrc + 1) # only old->c remains + + obj = None # releases pairwise (drops old->c and it_arr borrow) + self.assertEqual(r._lrc, base_lrc - 1) + + def test_three_nexts_full_sequence(self): + """ + Full sequence over [a,b,c,d,e,f] with batch of pairs: + next1 → (a,b): +3 (tuple a,b + old b) + next2 → (b,c): +2 (tuple b,c; old moves b→c) + next3 → (c,d): +2 (tuple c,d; old moves c→d) + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + base_lrc = r._lrc + + x = next(obj) + self.assertEqual(r._lrc, base_lrc + 3) + + y = next(obj) + self.assertEqual(r._lrc, base_lrc + 5) + + z = next(obj) + self.assertEqual(r._lrc, base_lrc + 7) + + x = None + self.assertEqual(r._lrc, base_lrc + 5) + + y = None + self.assertEqual(r._lrc, base_lrc + 3) + + z = None + self.assertEqual(r._lrc, base_lrc + 1) + + obj = None + self.assertEqual(r._lrc, base_lrc - 1) + + +class TestRegionPairwiseNextIntoRegion(unittest.TestCase): + """Tests for assigning next() results into the region.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_first_next_into_region_lrc(self): + """ + From commented block 3: + r.x = next(obj): LRC +1 from obj->tuple (owned by region) and +1 from old->right_element. + Since the tuple is owned by the region, its elements are not external borrows. + But old still holds an external ref to the right element. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + base_lrc = r._lrc + + # obj->result points to tuple that is now owned by region. LRC +1 + # obj->old points to right element of tuple, which is still an external borrow. LRC +1 + r.x = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + def test_second_next_local_after_first_into_region(self): + """ + From commented block 3: + After r.x = next(obj) (LRC base+1), y = next(obj): + yields (b,c), tuple borrowed externally (+2), old moves from b to c (-1+1=0 net for old). + But b was already in old (base+1 had old->b), so: + y's tuple borrows b (+1) and c (+1), old releases b (-1) and holds c (already in tuple). + Net: base+1 + 2(tuple y) - 1(old releases b) = base+2. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.arr = [r.a, r.b, r.c, r.d] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + base_lrc = r._lrc + + r.x = next(obj) + self.assertEqual(r._lrc, base_lrc + 2) + + y = next(obj) # (b,c): +2 for tuple, -1 from derefing obj->result to tuple + self.assertEqual(r._lrc, base_lrc + 2 + 1) + + r.x = None # region-owned tuple released; no external change + self.assertEqual(r._lrc, base_lrc + 2 + 1) + + y = None # external tuple (b,c) released: -2 + self.assertEqual(r._lrc, base_lrc + 1) # old->c... wait, old still holds c + + obj = None + self.assertEqual(r._lrc, base_lrc - 1) + + +class TestRegionPairwiseRelease(unittest.TestCase): + """Tests for releasing the pairwise iterator and its effect on LRC.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_release_unconsumed_pairwise_from_region_iter(self): + """ + Releasing a pairwise iterator that was never consumed (no next() calls) + should release the borrow on the underlying region iterator. + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + r.it_arr = iter(r.arr) + base_lrc = r._lrc + + obj = pairwise(r.it_arr) + self.assertEqual(r._lrc, base_lrc + 1) + + obj = None + self.assertEqual(r._lrc, base_lrc) + + def test_release_pairwise_after_partial_consumption(self): + """ + From commented block 1: + obj = None after consuming 3 pairs (with x, y released and r.z owned): + releases obj->it_arr and obj->old (-2 total from obj's own refs). + """ + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.d = self.A() + r.e = self.A() + r.f = self.A() + r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + base_lrc = r._lrc + + x = next(obj) # (a,b) +3 + y = next(obj) # (b,c) +2 → base+5 + r.z = next(obj) # (c,d) owned → base+5+1(old->d) = base+... let's track: + # r.z owned so tuple not borrowed externally, old moves c→d: +1(old->d)-1(old->c) + # net from base+5: base+5-1(old c release)+1(old d) = base+5 + + x = None # -2 → base+3 + y = None # -2 → base+1 (old still holds d) + r.z = None # region owned, net 0 → base+1 + + lrc_before_obj_none = r._lrc + obj = None # releases old->d and obj->it_arr + self.assertEqual(r._lrc, lrc_before_obj_none - 2) + + +class TestRegionPairwiseIsLocal(unittest.TestCase): + """Tests for locality of pairwise iterators and their yielded tuples.""" + + def setUp(self): + class A: pass + freeze(A()) + self.A = A + + def test_local_pairwise_is_local(self): + r = Region() + r.a = self.A() + r.b = self.A() + r.arr = [r.a, r.b] + it = iter(r.arr) + + obj = pairwise(it) + self.assertTrue(is_local(obj)) + + def test_next_result_is_local(self): + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + x = next(obj) + self.assertTrue(is_local(x)) + + def test_region_owned_pairwise_is_not_local(self): + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + r.it_arr = iter(r.arr) + + r.obj = pairwise(r.it_arr) + self.assertFalse(is_local(r.obj)) + + def test_region_owned_next_result_is_not_local(self): + r = Region() + r.a = self.A() + r.b = self.A() + r.c = self.A() + r.arr = [r.a, r.b, r.c] + r.it_arr = iter(r.arr) + + obj = pairwise(r.it_arr) + r.x = next(obj) + self.assertFalse(is_local(r.x)) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 80d16b2c03db70..0c217b8289d6a4 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -374,7 +374,8 @@ pairwise_next(PyObject *op) pairwiseobject *po = pairwiseobject_CAST(op); PyObject *it = po->it; PyObject *old = po->old; - PyObject *new, *result; + PyObject *new; + PyObject *result = po->result; if (it == NULL) { return NULL; @@ -404,42 +405,18 @@ pairwise_next(PyObject *op) return NULL; } - result = po->result; - if (Py_REFCNT(result) == 1 && PyRegion_IsLocal(result)) { - if(PyRegion_AddLocalRef(result)) { - PyRegion_RemoveLocalRef(old); - PyRegion_RemoveLocalRef(new); - Py_DECREF(old); - Py_DECREF(new); - return NULL; - } - Py_INCREF(result); - PyObject *last_old = PyTuple_GET_ITEM(result, 0); - PyObject *last_new = PyTuple_GET_ITEM(result, 1); - PyObject *old_region = PyRegion_NewRef(old); - PyObject *new_region = PyRegion_NewRef(new); - if(PyRegion_TakeRefs(result, old_region, new_region)) { - PyRegion_RemoveLocalRef(old_region); - PyRegion_RemoveLocalRef(new_region); - Py_DECREF(old_region); - Py_DECREF(new_region); - PyRegion_RemoveLocalRef(result); - Py_DECREF(result); - return NULL; - } - PyTuple_SET_ITEM(result, 0, old_region); - PyTuple_SET_ITEM(result, 1, new_region); - PyRegion_RemoveLocalRef(last_old); - Py_DECREF(last_old); - PyRegion_RemoveLocalRef(last_new); - Py_DECREF(last_new); - // bpo-42536: The GC may have untracked this result tuple. Since we're - // recycling it, make sure it's tracked again: - _PyTuple_Recycle(result); - } - else { - result = PyTuple_New(2); - if (result != NULL) { + if(result != NULL) { + if (Py_REFCNT(result) == 1 && PyRegion_IsLocal(result)) { + if(PyRegion_AddLocalRef(result)) { + PyRegion_RemoveLocalRef(old); + PyRegion_RemoveLocalRef(new); + Py_DECREF(old); + Py_DECREF(new); + return NULL; + } + Py_INCREF(result); + PyObject *last_old = PyTuple_GET_ITEM(result, 0); + PyObject *last_new = PyTuple_GET_ITEM(result, 1); PyObject *old_region = PyRegion_NewRef(old); PyObject *new_region = PyRegion_NewRef(new); if(PyRegion_TakeRefs(result, old_region, new_region)) { @@ -453,13 +430,44 @@ pairwise_next(PyObject *op) } PyTuple_SET_ITEM(result, 0, old_region); PyTuple_SET_ITEM(result, 1, new_region); + PyRegion_RemoveLocalRef(last_old); + Py_DECREF(last_old); + PyRegion_RemoveLocalRef(last_new); + Py_DECREF(last_new); + // bpo-42536: The GC may have untracked this result tuple. Since we're + // recycling it, make sure it's tracked again: + _PyTuple_Recycle(result); + goto end; + } + else { + PyRegion_CLEAR(po, po->result); } } - PyRegion_XSETREF(po, po->old, new); - // Py_XSETREF(po->old, new); - PyRegion_RemoveLocalRef(old); - Py_DECREF(old); - return result; + + result = PyTuple_New(2); + if (result != NULL) { + PyObject *old_region = PyRegion_NewRef(old); + PyObject *new_region = PyRegion_NewRef(new); + if(PyRegion_TakeRefs(result, old_region, new_region)) { + PyRegion_RemoveLocalRef(old_region); + PyRegion_RemoveLocalRef(new_region); + Py_DECREF(old_region); + Py_DECREF(new_region); + PyRegion_RemoveLocalRef(result); + Py_DECREF(result); + return NULL; + } + PyTuple_SET_ITEM(result, 0, old_region); + PyTuple_SET_ITEM(result, 1, new_region); + goto end; + } + + end: + PyRegion_XSETREF(po, po->old, new); + // Py_XSETREF(po->old, new); + PyRegion_RemoveLocalRef(old); + Py_DECREF(old); + return result; } static PyType_Slot pairwise_slots[] = { diff --git a/test_code/enum_test/create_enum.py b/test_code/enum_test/create_enum.py index e1cce5fcf19dbc..e15013523a78cd 100644 --- a/test_code/enum_test/create_enum.py +++ b/test_code/enum_test/create_enum.py @@ -14,10 +14,6 @@ class A: pass r.d = A() r.e = A() -# arr = [r.a, r.b, r.c, r.d, r.e] -# print(f"Region r after creating arr: {r}") -# r.arr = arr -# print(f"Region r after moving arr: {r}") #------------------Problem with LRC should not be increases------------------ # print(f"Region r: {r}") @@ -45,28 +41,36 @@ class A: pass # print(f"Region r after deleting obj: {r}") #------------------Problem with LRC not being decreased------------------ -# print(f"Region r: {r}") -# r.arr = [r.a, r.b] -# print(f"Region r after creating arr: {r}") -# # input("Press Enter to create enum...") -# r.it_arr = iter(r.arr) -# print(f"Region r after creating iterator: {r}") -# # input("Press Enter to create enum...") -# obj = enumerate(r.it_arr) # LRC +1 since obj points to r.it_arr -# print(f"Region r after creating enum: {r}") -# # input("Press Enter to create enum...") -# re1 = next(obj) -# print(f"{is_local(obj)}") -# print(f"Region r after getting next from enum: {r}") -# # input("Press Enter to create enum...") -# re2 = next(obj) -# print(f"Region r after getting next from enum: {r}") -# re1 = None # PROBLEM: LRC does not decrease, which should not be since we remove ref. -# print(f"Region r after deleting re1: {r}") -# re2 = None -# print(f"Region r after deleting re2: {r}") -# obj = None -# print(f"Region r after deleting obj: {r}") +print(f"Region r: {r}") +r.arr = [r.a, r.b, r.c, r.d] +print(f"Region r after creating arr: {r}") +# input("Press Enter to create enum...") +r.it_arr = iter(r.arr) +print(f"Region r after creating iterator: {r}") +# input("Press Enter to create enum...") +obj = enumerate(r.it_arr) # LRC +1 since obj points to r.it_arr +print(f"Region r after creating enum: {r}") +# input("Press Enter to create enum...") +r.re1 = next(obj) +print(f"{is_local(obj)}") +print(f"Region r after getting next from enum: {r}") +# input("Press Enter to create enum...") +re2 = next(obj) +print(f"Region r after getting next from enum: {r}") +r.re3 = next(obj) +print(f"Region r after getting next from enum: {r}") +re4 = next(obj) +print(f"Region r after getting next from enum: {r}") +r.re1 = None +print(f"Region r after deleting re1: {r}") +re2 = None +print(f"Region r after deleting re2: {r}") +r.re3 = None +print(f"Region r after deleting re3: {r}") +re4 = None +print(f"Region r after deleting re4: {r}") +obj = None +print(f"Region r after deleting obj: {r}") #------------------Problem with GC------------------ # print(f"Region r: {r}") diff --git a/test_code/itertool_test/itertool_pairwise1.py b/test_code/itertool_test/itertool_pairwise1.py index f883e17651bd0a..fd89fb3703f5c8 100644 --- a/test_code/itertool_test/itertool_pairwise1.py +++ b/test_code/itertool_test/itertool_pairwise1.py @@ -4,7 +4,7 @@ class A: pass freeze(A()) -# freeze(batched) +freeze(pairwise) r = Region() r.a = A() r.b = A() @@ -13,73 +13,80 @@ class A: pass r.e = A() r.f = A() -r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] -print(f"Region r after creating arr: {r}") -r.it_arr = iter(r.arr) -print(f"Region r after creating iterator: {r}") -obj = pairwise(r.it_arr) -print(f"Region r after creating pairwise iterator: {r}") -input("Press Enter to assign pairwise iterator to r.obj...") -x = next(obj) -print(f"Region r after getting next from pairwise iterator: {r}") -print(f"x: {x}") -y = next(obj) -print(f"Region r after getting next from pairwise iterator: {r}") -print(f"y: {y}") -r.z = next(obj) -print(f"Region r after getting next from pairwise iterator: {r}") -print(f"z: {r.z}") -x = None -print(f"Region r after deleting x: {r}") -y = None -print(f"Region r after deleting y: {r}") -r.z = None -print(f"Region r after deleting z: {r}") -obj = None -print(f"Region r after deleting obj: {r}") - # r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] # print(f"Region r after creating arr: {r}") # r.it_arr = iter(r.arr) # print(f"Region r after creating iterator: {r}") -# input("Press Enter to create pairwise iterator...") # obj = pairwise(r.it_arr) # print(f"Region r after creating pairwise iterator: {r}") # input("Press Enter to assign pairwise iterator to r.obj...") -# r.obj = obj -# print(f"Region r after assigning pairwise iterator to r.obj: {r}") -# r.x = next(r.obj) +# x = next(obj) # LRC +2 from tuple to 2 elements in the region, and +1 from obj to old (right element of tuple) # print(f"Region r after getting next from pairwise iterator: {r}") -# print(f"x: {r.x}") -# y = next(r.obj) +# print(f"x: {x}") +# y = next(obj) # LRC +2 # print(f"Region r after getting next from pairwise iterator: {r}") # print(f"y: {y}") -# r.x = None +# r.z = next(obj) # LRC +0 +# print(f"Region r after getting next from pairwise iterator: {r}") +# print(f"z: {r.z}") +# x = None # LRC -2 # print(f"Region r after deleting x: {r}") -# y = None +# y = None # LRC -2 # print(f"Region r after deleting y: {r}") -# obj = None +# r.z = None # LRC -0 +# print(f"Region r after deleting z: {r}") +# obj = None # LRC -2 from deleting ref from obj to r.it and obj to old # print(f"Region r after deleting obj: {r}") # r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] # print(f"Region r after creating arr: {r}") # r.it_arr = iter(r.arr) # print(f"Region r after creating iterator: {r}") -# input("Press Enter to create pairwise iterator...") # obj = pairwise(r.it_arr) # print(f"Region r after creating pairwise iterator: {r}") # input("Press Enter to assign pairwise iterator to r.obj...") -# r.obj = obj -# print(f"Region r after assigning pairwise iterator to r.obj: {r}") -# x = next(r.obj) +# x = next(obj) # LRC +3 # print(f"Region r after getting next from pairwise iterator: {r}") # print(f"x: {x}") -# r.y = next(r.obj) -# print(f"Region r after getting next from pairwise iterator: {r}") -# print(f"y: {r.y}") # x = None # print(f"Region r after deleting x: {r}") -# r.y = None +# x = next(obj) # LRC +0 since everything is already set from the previous x = next(obj) +# print(f"Region r after getting next from pairwise iterator: {r}") +# print(f"x: {x}") +# y = next(obj) # LRC+2 +# print(f"Region r after getting next from pairwise iterator: {r}") +# print(f"y: {y}") +# x = None # LRC -2 +# print(f"Region r after deleting x: {r}") +# y = None # LRC -2 # print(f"Region r after deleting y: {r}") -# obj = None -# print(f"Region r after deleting obj: {r}") \ No newline at end of file +# obj = None # LRC -2 from deleting ref from obj to r.it and obj to old +# print(f"Region r after deleting obj: {r}") + +# r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] +# print(f"Region r after creating arr: {r}") +# r.it_arr = iter(r.arr) +# print(f"Region r after creating iterator: {r}") +# # input("Press Enter to create pairwise iterator...") +# obj = pairwise(r.it_arr) # LRC +1 +# print(f"Region r after creating pairwise iterator: {r}") +# # input("Press Enter to assign pairwise iterator to r.obj...") +# r.x = next(obj) # LRC +1 from obj to tuple, and +1 from obj->old to right element of tuple +# print(f"Region r after getting next from pairwise iterator: {r}") +# print(f"x: {r.x}") +# y = next(obj) # LRC +2 bacause of tuple to 2 elements in the region, -1 from deleting ref from obj->result to tuple +# print(f"Region r after getting next from pairwise iterator: {r}") +# print(f"y: {y}") +# r.x = None # LRC -0 +# print(f"Region r after deleting x: {r}") +# y = None # LRC -2 +# print(f"Region r after deleting y: {r}") +# obj = None # LRC -2 from deleting ref from obj to r.it and -1 from obj to old +# print(f"Region r after deleting obj: {r}") + +r.arr = [r.a, r.b, r.c, r.d, r.e, r.f] +print(f"Region r after creating arr: {r}") +r.it_arr = iter(r.arr) +print(f"Region r after creating iterator: {r}") +r.obj = pairwise(r.it_arr) +print(f"Region r after creating pairwise iterator: {r}") \ No newline at end of file diff --git a/test_code/report_code/onlygil.py b/test_code/report_code/onlygil.py new file mode 100644 index 00000000000000..b334d699301b3e --- /dev/null +++ b/test_code/report_code/onlygil.py @@ -0,0 +1,33 @@ +import time +import threading +import _interpreters # Python 3.12+ +import sys + +print(sys.version) + +def cpu_task(): + total = 0 + for i in range(2_000_000): + total += i ** 2 + return total + +# --- METHOD A: threading (your current approach) --- +start = time.perf_counter() +t1 = threading.Thread(target=cpu_task) +t2 = threading.Thread(target=cpu_task) +t1.start(); t2.start() +t1.join(); t2.join() +thread_time = time.perf_counter() - start + +# --- METHOD B: sequential (baseline) --- +start = time.perf_counter() +cpu_task() +cpu_task() +sequential_time = time.perf_counter() - start + +print(f"Sequential : {sequential_time:.3f}s") +print(f"Threading : {thread_time:.3f}s") +print(f"Speedup : {sequential_time / thread_time:.2f}x") +print() +print("If speedup ≈ 1.0 → threads gave NO parallelism benefit (GIL bottleneck)") +print("If speedup ≈ 2.0 → true parallelism achieved") From fdeed5967b863a893092e0b2295518722abf855a Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Thu, 2 Apr 2026 14:55:07 +0200 Subject: [PATCH 35/37] migrate to support printing the np array in numpy --- Objects/stringlib/unicode_format.h | 11 +++++++++-- test_code/report_code/test.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 test_code/report_code/test.py diff --git a/Objects/stringlib/unicode_format.h b/Objects/stringlib/unicode_format.h index ff32db65b11a0b..8282bfc670b7b1 100644 --- a/Objects/stringlib/unicode_format.h +++ b/Objects/stringlib/unicode_format.h @@ -418,6 +418,7 @@ get_field_object(SubString *input, PyObject *args, PyObject *kwargs, } if (kwargs == NULL) { PyErr_SetObject(PyExc_KeyError, key); + assert(PyRegion_IsLocal(key)); Py_DECREF(key); goto error; } @@ -425,6 +426,7 @@ get_field_object(SubString *input, PyObject *args, PyObject *kwargs, code is no longer just used with kwargs. It might be passed a non-dict when called through format_map. */ obj = PyObject_GetItem(kwargs, key); + assert(PyRegion_IsLocal(key)); Py_DECREF(key); if (obj == NULL) { goto error; @@ -474,12 +476,14 @@ get_field_object(SubString *input, PyObject *args, PyObject *kwargs, goto error; /* assign to obj */ - Py_SETREF(obj, tmp); + PyRegion_XSETLOCALREF(obj, tmp); + // Py_SETREF(obj, tmp); } /* end of iterator, this is the non-error case */ if (ok == 1) return obj; error: + PyRegion_RemoveLocalRef(obj); Py_XDECREF(obj); return NULL; } @@ -825,7 +829,8 @@ output_markup(SubString *field_name, SubString *format_spec, goto done; /* do the assignment, transferring ownership: fieldobj = tmp */ - Py_SETREF(fieldobj, tmp); + PyRegion_XSETLOCALREF(fieldobj, tmp); + // Py_SETREF(fieldobj, tmp); tmp = NULL; } @@ -851,6 +856,8 @@ output_markup(SubString *field_name, SubString *format_spec, result = 1; done: + PyRegion_RemoveLocalRef(fieldobj); + PyRegion_RemoveLocalRef(tmp); Py_XDECREF(fieldobj); Py_XDECREF(tmp); diff --git a/test_code/report_code/test.py b/test_code/report_code/test.py new file mode 100644 index 00000000000000..8362d24c9094fb --- /dev/null +++ b/test_code/report_code/test.py @@ -0,0 +1,19 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +class A: pass +freeze(A()) + +r = Region() +print(f"Initial Region: {r}") +r.a = A() +r.b = A() +r.c = A() +r.d = A() + +arr = [r.a, r.b, r.c, r.d] +print(f"Region r after creating arr: {r}") +it_arr = iter(arr) +print(f"Region r after creating iterator: {r}") +r.it_arr = it_arr +print(f"Region r after assigning iterator to region: {r}") \ No newline at end of file From 9b4c2222934d103671e80867c95822da047cef1a Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Fri, 3 Apr 2026 15:12:59 +0200 Subject: [PATCH 36/37] fix bug in setiter_iternext. --- Objects/setobject.c | 15 +++++++++++---- test_code/set_test/set_itertestprint.py | 23 +++++++++++++++++++++++ test_code/set_test/setior_test1.py | 13 ++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 test_code/set_test/set_itertestprint.py diff --git a/Objects/setobject.c b/Objects/setobject.c index 7b351be4a5b9cc..0790d8f835ffd8 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1027,16 +1027,23 @@ static PyObject *setiter_iternext(PyObject *self) i++; } if (i <= mask) { - key = PyRegion_NewRef(entry[i].key); + // key = PyRegion_NewRef(entry[i].key); // Incorrect since we want to return NULL immediately, not set key to NULL. // key = Py_NewRef(entry[i].key); + key = entry[i].key; + if (PyRegion_AddLocalRef(key)) { + return NULL; + } + Py_INCREF(key); } // Cannot return before unlocking Py_END_CRITICAL_SECTION(); si->si_pos = i+1; if (key == NULL) { - si->si_set = NULL; - PyRegion_RemoveLocalRef(so); - Py_DECREF(so); + PyRegion_CLEAR(si, si->si_set); + // Since so is si->si_set, PyRegion_RemoveRef should be used, not PyRegion_RemoveLocalRef + // PyRegion_RemoveRef(si, so); + // si->si_set = NULL; + // Py_DECREF(so); return NULL; } si->len--; diff --git a/test_code/set_test/set_itertestprint.py b/test_code/set_test/set_itertestprint.py new file mode 100644 index 00000000000000..99a1470c5f4bf0 --- /dev/null +++ b/test_code/set_test/set_itertestprint.py @@ -0,0 +1,23 @@ +from regions import Region, is_local +from immutable import freeze, register_freezable + +r = Region() +print(f"Initial Region: {r}") +class A: pass +freeze(A()) + +r.word=A() +r.word2=A() +r.word3=A() +r.word4=A() + +r.arr = [r.word, r.word2, r.word3, r.word4] +print(f"Before for loop: {r}") +s = set(r.arr) +print(f"Before for loop: {r}") + +for i in r.arr: + print(hex(id(i))) +i = None + +print(f"After for loop: {r}") \ No newline at end of file diff --git a/test_code/set_test/setior_test1.py b/test_code/set_test/setior_test1.py index d1bf854e626568..a44cd75abc68f9 100644 --- a/test_code/set_test/setior_test1.py +++ b/test_code/set_test/setior_test1.py @@ -39,4 +39,15 @@ class A: pass; r.s1 |= s2 # r.s1.update(s2) print(f"Region after creating set or: {r}") -print(f"Or result: {r.s1}") \ No newline at end of file +print(f"Or result: {r.s1}") + +# r1 = Region() +# r1.a = {1, 2} +# r2 = Region() +# r2.b = {2, 3} + +# print(f"r1: {r1}") +# print(f"r2: {r2}") +# r1.a |= r2.b +# print(f"r1: {r1}") +# print(f"r2: {r2}") \ No newline at end of file From 3a624938fc58679cd7080d8bc9f2d27313ab4ca3 Mon Sep 17 00:00:00 2001 From: CaQtimlTP_WSL Date: Wed, 8 Apr 2026 11:29:55 +0200 Subject: [PATCH 37/37] update XSETLOCALREF and XSETREF to handle error from firing the barrier --- Modules/itertoolsmodule.c | 10 ++++++++-- Objects/setobject.c | 7 ++++++- Objects/stringlib/unicode_format.h | 12 ++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 0c217b8289d6a4..1d097a3b16d955 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -382,7 +382,9 @@ pairwise_next(PyObject *op) } if (old == NULL) { old = (*Py_TYPE(it)->tp_iternext)(it); - PyRegion_XSETREF(po, po->old, old); + if(PyRegion_XSETREF(po, po->old, old)) { + return NULL; + } // Py_XSETREF(po->old, old); if (old == NULL) { PyRegion_CLEAR(po, po->it); @@ -463,7 +465,11 @@ pairwise_next(PyObject *op) } end: - PyRegion_XSETREF(po, po->old, new); + if(PyRegion_XSETREF(po, po->old, new)) { + PyRegion_RemoveLocalRef(new); + Py_DECREF(new); + return NULL; + } // Py_XSETREF(po->old, new); PyRegion_RemoveLocalRef(old); Py_DECREF(old); diff --git a/Objects/setobject.c b/Objects/setobject.c index 0790d8f835ffd8..33bbebf9914ea2 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -637,6 +637,7 @@ set_repr_lock_held(PySetObject *so) while (set_next(so, &pos, &entry)) { PyObject *entry_key = PyRegion_NewRef(entry->key); if(entry_key == NULL) { + assert(PyRegion_IsLocal(keys)); Py_DECREF(keys); goto done; } @@ -1716,7 +1717,11 @@ set_intersection_multi_impl(PySetObject *so, PyObject * const *others, Py_DECREF(result); return NULL; } - PyRegion_XSETLOCALREF(result, newresult); + if(PyRegion_XSETLOCALREF(result, newresult)) { + PyRegion_RemoveLocalRef(result); + Py_DECREF(result); + return NULL; + } // Py_SETREF(result, newresult); } return result; diff --git a/Objects/stringlib/unicode_format.h b/Objects/stringlib/unicode_format.h index 8282bfc670b7b1..ff255554493b30 100644 --- a/Objects/stringlib/unicode_format.h +++ b/Objects/stringlib/unicode_format.h @@ -476,7 +476,11 @@ get_field_object(SubString *input, PyObject *args, PyObject *kwargs, goto error; /* assign to obj */ - PyRegion_XSETLOCALREF(obj, tmp); + if(PyRegion_XSETLOCALREF(obj, tmp)) { + PyRegion_RemoveLocalRef(tmp); + Py_DECREF(tmp); + goto error; + } // Py_SETREF(obj, tmp); } /* end of iterator, this is the non-error case */ @@ -829,7 +833,11 @@ output_markup(SubString *field_name, SubString *format_spec, goto done; /* do the assignment, transferring ownership: fieldobj = tmp */ - PyRegion_XSETLOCALREF(fieldobj, tmp); + if(PyRegion_XSETLOCALREF(fieldobj, tmp)) { + PyRegion_RemoveLocalRef(tmp); + Py_DECREF(tmp); + goto done; + } // Py_SETREF(fieldobj, tmp); tmp = NULL; }