Skip to content

Commit b24bd15

Browse files
committed
Pyrona: Add write barrier to Python/bytecodes.c
1 parent 3f529da commit b24bd15

3 files changed

Lines changed: 500 additions & 286 deletions

File tree

Lib/test/test_veronapy_writebarrier.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,163 @@ def test_cell_replace_same_bridge(self):
7676
# since this writes replaces the previous owning reference
7777
c.cell_contents = child
7878

79+
# It's not possible to test the existance of all bytecodes. The dis only
80+
# shows the unoptimized version of the bytecode. For testing specilized or
81+
# optimized it's therefore not possible to check for the presence of that
82+
# bytecode in the `dis`. Instead we can only check for the effects of the
83+
# bytecodes.
84+
#
85+
# During test development it's also possible to verify that the expected
86+
# bytecode is triggered by setting a breakpoint in it. Note that it's also
87+
# run during the startup of the runtime and by other tests, so make sure
88+
# the test in question actually started, when the breakpoint is triggered.
89+
class TestBytecodeWriteBarrier(unittest.TestCase):
90+
# Define a class with __slots__
91+
class ClassWithSlots:
92+
# Use `__slots__` to get the `STORE_ATTR_SLOT` bytecode
93+
__slots__ = ('slot',)
94+
95+
def __init__(self):
96+
self.slot = None
97+
98+
class ClassWithAttr:
99+
def __init__(self):
100+
self.attr = None
101+
102+
def setUp(self):
103+
# This freezes A and super and meta types of A namely `type` and `object`
104+
makeimmutable(self.ClassWithSlots)
105+
makeimmutable(self.ClassWithAttr)
106+
107+
def test_delete_deref(self):
108+
r = Region()
109+
r.field = {}
110+
x = r.field
111+
112+
# Make sure the region knows about local reference from x
113+
self.assertFalse(r.try_close())
114+
115+
def del_x():
116+
nonlocal x
117+
del x # This triggers the DELETE_DEREF opcode
118+
119+
# Create the function and disassemble to confirm DELETE_DEREF is present
120+
bytecode = dis.Bytecode(del_x)
121+
self.assertIn("DELETE_DEREF", [instr.opname for instr in bytecode])
122+
123+
self.assertTrue(r.is_open())
124+
125+
# Call the function forcing the deletion of x which should
126+
# close the region.
127+
del_x()
128+
129+
self.assertFalse(r.is_open())
130+
131+
# This tests that references stored by `STORE_ATTR_SLOT` are known
132+
# by the write barrier
133+
def test_store_attr_slot_add_reference(self):
134+
def set_value(obj, val):
135+
obj.slot = val # This triggers the STORE_ATTR_SLOT opcode
136+
137+
# Setup a region and a class with slots
138+
r = Region()
139+
r.slots = self.ClassWithSlots()
140+
self.assertTrue(r.owns_object(r.slots))
141+
142+
# Run `set_value` multiple times to optimize it
143+
set_value(r.slots, None)
144+
set_value(r.slots, {})
145+
146+
# Create a new local object
147+
new_object = {}
148+
new_object["data"] = {}
149+
150+
# Store the object in slots
151+
set_value(r.slots, new_object)
152+
153+
# Verify the region ownership
154+
self.assertTrue(r.owns_object(new_object))
155+
self.assertTrue(r.owns_object(new_object["data"]))
156+
157+
# This tests that references removed by `STORE_ATTR_SLOT` are known
158+
# by the write barrier
159+
def test_store_attr_slot_remove_reference(self):
160+
def set_value(obj, val):
161+
obj.slot = val # This triggers the STORE_ATTR_SLOT opcode
162+
163+
# Setup a region and a class with slots
164+
r = Region()
165+
r.data = {}
166+
slots = self.ClassWithSlots()
167+
168+
# Run `set_value` multiple times to optimize it
169+
set_value(slots, None)
170+
set_value(slots, {})
171+
172+
# Create local reference into the region
173+
set_value(slots, r.data)
174+
175+
# Make sure the region knows about the reference from the slot
176+
self.assertFalse(r.try_close())
177+
178+
# Clear the reference from slots
179+
set_value(slots, None)
180+
181+
# Check that the region was closed.
182+
self.assertFalse(r.is_open())
183+
184+
# This tests that references stored by `STORE_ATTR_INSTANCE_VALUE` are known
185+
# by the write barrier
186+
def test_store_attr_instance_value_add_reference(self):
187+
def set_value(obj, val):
188+
obj.attr = val # This triggers the STORE_ATTR_INSTANCE_VALUE opcode
189+
190+
# Setup a region and a class with attributes
191+
r = Region()
192+
r.attr = self.ClassWithAttr()
193+
self.assertTrue(r.owns_object(r.attr))
194+
195+
# Run `set_value` multiple times to optimize it
196+
set_value(r.attr, None)
197+
set_value(r.attr, {})
198+
199+
# Create a new local object
200+
new_object = {}
201+
new_object["data"] = {}
202+
203+
# Store the object in attributes
204+
set_value(r.attr, new_object)
205+
206+
# Verify the region ownership
207+
self.assertTrue(r.owns_object(new_object))
208+
self.assertTrue(r.owns_object(new_object["data"]))
209+
210+
# This tests that references removed by `STORE_ATTR_INSTANCE_VALUE` are known
211+
# by the write barrier
212+
def test_store_attr_instance_value_remove_reference(self):
213+
def set_value(obj, val):
214+
obj.attr = val # This triggers the STORE_ATTR_INSTANCE_VALUE opcode
215+
216+
# Setup a region and a class with attributes
217+
r = Region()
218+
r.data = {}
219+
attr = self.ClassWithAttr()
220+
221+
# Run `set_value` multiple times to optimize it
222+
set_value(attr, None)
223+
set_value(attr, {})
224+
225+
# Create local reference into the region
226+
set_value(attr, r.data)
227+
228+
# Make sure the region knows about the reference from the slot
229+
self.assertFalse(r.try_close())
230+
231+
# Clear the reference from attributes
232+
set_value(attr, None)
233+
234+
# Check that the region was closed.
235+
self.assertFalse(r.is_open())
236+
237+
if __name__ == "__main__":
238+
unittest.main()

Python/bytecodes.c

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,6 +1980,13 @@ dummy_func(
19801980
}
19811981
PyDictValues *values = _PyDictOrValues_GetValues(dorv);
19821982
PyObject *old_value = values->values[index];
1983+
1984+
// Check that the reference can be created
1985+
if (!Py_REGIONCHANGEREFERENCE(owner, old_value, value)) {
1986+
Py_DECREF(owner);
1987+
goto error;
1988+
}
1989+
19831990
values->values[index] = value;
19841991
if (old_value == NULL) {
19851992
_PyDictValues_AddToInsertionOrder(values, index);
@@ -2015,23 +2022,33 @@ dummy_func(
20152022
DEOPT_IF(ep->me_key != name, STORE_ATTR);
20162023
old_value = _PyDictEntry_Value(ep);
20172024
DEOPT_IF(old_value == NULL, STORE_ATTR);
2018-
new_version = _PyDict_NotifyEvent(tstate->interp, PyDict_EVENT_MODIFIED, dict, name, value);
2019-
if(_PyDictEntry_IsImmutable(ep)){
2025+
if (_PyDictEntry_IsImmutable(ep)) {
20202026
format_exc_notwriteable(tstate, frame->f_code, oparg);
2027+
Py_DECREF(owner);
20212028
goto error;
20222029
}
2030+
if (!Py_REGIONCHANGEREFERENCE(owner, old_value, value)) {
2031+
Py_DECREF(owner);
2032+
goto error;
2033+
}
2034+
new_version = _PyDict_NotifyEvent(tstate->interp, PyDict_EVENT_MODIFIED, dict, name, value);
20232035
_PyDictEntry_SetValue(ep, value);
20242036
}
20252037
else {
20262038
PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + hint;
20272039
DEOPT_IF(ep->me_key != name, STORE_ATTR);
20282040
old_value = _PyDictEntry_Value(ep);
20292041
DEOPT_IF(old_value == NULL, STORE_ATTR);
2030-
new_version = _PyDict_NotifyEvent(tstate->interp, PyDict_EVENT_MODIFIED, dict, name, value);
2031-
if(_PyDictEntry_IsImmutable(ep)){
2042+
if (_PyDictEntry_IsImmutable(ep)) {
20322043
format_exc_notwriteable(tstate, frame->f_code, oparg);
2044+
Py_DECREF(owner);
2045+
goto error;
2046+
}
2047+
if (!Py_REGIONCHANGEREFERENCE(owner, old_value, value)) {
2048+
Py_DECREF(owner);
20332049
goto error;
20342050
}
2051+
new_version = _PyDict_NotifyEvent(tstate->interp, PyDict_EVENT_MODIFIED, dict, name, value);
20352052
_PyDictEntry_SetValue(ep, value);
20362053
}
20372054
Py_DECREF(old_value);
@@ -2055,9 +2072,16 @@ dummy_func(
20552072
Py_DECREF(owner);
20562073
goto error;
20572074
}
2075+
20582076
char *addr = (char *)owner + index;
20592077
STAT_INC(STORE_ATTR, hit);
20602078
PyObject *old_value = *(PyObject **)addr;
2079+
// Check that the reference can be created
2080+
if (!Py_REGIONCHANGEREFERENCE(owner, old_value, value)) {
2081+
Py_DECREF(owner);
2082+
goto error;
2083+
}
2084+
20612085
*(PyObject **)addr = value;
20622086
Py_XDECREF(old_value);
20632087
Py_DECREF(owner);

0 commit comments

Comments
 (0)