Skip to content

Commit b5ee025

Browse files
authored
gh-115999: Specialize LOAD_ATTR for instance and class receivers in free-threaded builds (#128164)
Finish specialization for LOAD_ATTR in the free-threaded build by adding support for class and instance receivers.
1 parent 1c13c56 commit b5ee025

18 files changed

+608
-260
lines changed

Include/cpython/pystats.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
#define PYSTATS_MAX_UOP_ID 512
3333

34-
#define SPECIALIZATION_FAILURE_KINDS 36
34+
#define SPECIALIZATION_FAILURE_KINDS 37
3535

3636
/* Stats for determining who is calling PyEval_EvalFrame */
3737
#define EVAL_CALL_TOTAL 0

Include/internal/pycore_dict.h

+10
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,16 @@ extern Py_ssize_t _Py_dict_lookup_threadsafe_stackref(PyDictObject *mp, PyObject
114114

115115
extern Py_ssize_t _PyDict_LookupIndex(PyDictObject *, PyObject *);
116116
extern Py_ssize_t _PyDictKeys_StringLookup(PyDictKeysObject* dictkeys, PyObject *key);
117+
118+
/* Look up a string key in an all unicode dict keys, assign the keys object a version, and
119+
* store it in version.
120+
*
121+
* Returns DKIX_ERROR if key is not a string or if the keys object is not all
122+
* strings.
123+
*
124+
* Returns DKIX_EMPTY if the key is not present.
125+
*/
126+
extern Py_ssize_t _PyDictKeys_StringLookupAndVersion(PyDictKeysObject* dictkeys, PyObject *key, uint32_t *version);
117127
extern Py_ssize_t _PyDictKeys_StringLookupSplit(PyDictKeysObject* dictkeys, PyObject *key);
118128
PyAPI_FUNC(PyObject *)_PyDict_LoadGlobal(PyDictObject *, PyDictObject *, PyObject *);
119129
PyAPI_FUNC(void) _PyDict_LoadGlobalStackRef(PyDictObject *, PyDictObject *, PyObject *, _PyStackRef *);

Include/internal/pycore_opcode_metadata.h

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_uop_metadata.h

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_capi/test_type.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from test.support import import_helper
1+
from test.support import import_helper, Py_GIL_DISABLED, refleak_helper
22
import unittest
33

44
_testcapi = import_helper.import_module('_testcapi')
@@ -37,6 +37,9 @@ class D(A, C): pass
3737
# as well
3838
type_freeze(D)
3939

40+
@unittest.skipIf(
41+
Py_GIL_DISABLED and refleak_helper.hunting_for_refleaks(),
42+
"Specialization failure triggers gh-127773")
4043
def test_freeze_meta(self):
4144
"""test PyType_Freeze() with overridden MRO"""
4245
type_freeze = _testcapi.type_freeze

Lib/test/test_descr.py

+26-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import random
88
import string
99
import sys
10+
import textwrap
1011
import types
1112
import unittest
1213
import warnings
@@ -15,6 +16,7 @@
1516
from copy import deepcopy
1617
from contextlib import redirect_stdout
1718
from test import support
19+
from test.support.script_helper import assert_python_ok
1820

1921
try:
2022
import _testcapi
@@ -5222,6 +5224,7 @@ def test_type_lookup_mro_reference(self):
52225224
# Issue #14199: _PyType_Lookup() has to keep a strong reference to
52235225
# the type MRO because it may be modified during the lookup, if
52245226
# __bases__ is set during the lookup for example.
5227+
code = textwrap.dedent("""
52255228
class MyKey(object):
52265229
def __hash__(self):
52275230
return hash('mykey')
@@ -5237,12 +5240,29 @@ class Base2(object):
52375240
mykey = 'from Base2'
52385241
mykey2 = 'from Base2'
52395242
5240-
with self.assertWarnsRegex(RuntimeWarning, 'X'):
5241-
X = type('X', (Base,), {MyKey(): 5})
5242-
# mykey is read from Base
5243-
self.assertEqual(X.mykey, 'from Base')
5244-
# mykey2 is read from Base2 because MyKey.__eq__ has set __bases__
5245-
self.assertEqual(X.mykey2, 'from Base2')
5243+
X = type('X', (Base,), {MyKey(): 5})
5244+
5245+
bases_before = ",".join([c.__name__ for c in X.__bases__])
5246+
print(f"before={bases_before}")
5247+
5248+
# mykey is initially read from Base, however, the lookup will be perfomed
5249+
# again if specialization fails. The second lookup will use the new
5250+
# mro set by __eq__.
5251+
print(X.mykey)
5252+
5253+
bases_after = ",".join([c.__name__ for c in X.__bases__])
5254+
print(f"after={bases_after}")
5255+
5256+
# mykey2 is read from Base2 because MyKey.__eq__ has set __bases_
5257+
print(f"mykey2={X.mykey2}")
5258+
""")
5259+
_, out, err = assert_python_ok("-c", code)
5260+
err = err.decode()
5261+
self.assertRegex(err, "RuntimeWarning: .*X")
5262+
out = out.decode()
5263+
self.assertRegex(out, "before=Base")
5264+
self.assertRegex(out, "after=Base2")
5265+
self.assertRegex(out, "mykey2=from Base2")
52465266

52475267

52485268
class PicklingTests(unittest.TestCase):

Lib/test/test_generated_cases.py

+40-34
Original file line numberDiff line numberDiff line change
@@ -1639,35 +1639,45 @@ def test_escaping_call_next_to_cmacro(self):
16391639
"""
16401640
self.run_cases_test(input, output)
16411641

1642-
def test_pop_dead_inputs_all_live(self):
1642+
def test_pystackref_frompyobject_new_next_to_cmacro(self):
16431643
input = """
1644-
inst(OP, (a, b --)) {
1645-
POP_DEAD_INPUTS();
1646-
HAM(a, b);
1647-
INPUTS_DEAD();
1644+
inst(OP, (-- out1, out2)) {
1645+
PyObject *obj = SPAM();
1646+
#ifdef Py_GIL_DISABLED
1647+
out1 = PyStackRef_FromPyObjectNew(obj);
1648+
#else
1649+
out1 = PyStackRef_FromPyObjectNew(obj);
1650+
#endif
1651+
out2 = PyStackRef_FromPyObjectNew(obj);
16481652
}
16491653
"""
16501654
output = """
16511655
TARGET(OP) {
16521656
frame->instr_ptr = next_instr;
16531657
next_instr += 1;
16541658
INSTRUCTION_STATS(OP);
1655-
_PyStackRef a;
1656-
_PyStackRef b;
1657-
b = stack_pointer[-1];
1658-
a = stack_pointer[-2];
1659-
HAM(a, b);
1660-
stack_pointer += -2;
1659+
_PyStackRef out1;
1660+
_PyStackRef out2;
1661+
PyObject *obj = SPAM();
1662+
#ifdef Py_GIL_DISABLED
1663+
out1 = PyStackRef_FromPyObjectNew(obj);
1664+
#else
1665+
out1 = PyStackRef_FromPyObjectNew(obj);
1666+
#endif
1667+
out2 = PyStackRef_FromPyObjectNew(obj);
1668+
stack_pointer[0] = out1;
1669+
stack_pointer[1] = out2;
1670+
stack_pointer += 2;
16611671
assert(WITHIN_STACK_BOUNDS());
16621672
DISPATCH();
16631673
}
16641674
"""
16651675
self.run_cases_test(input, output)
16661676

1667-
def test_pop_dead_inputs_some_live(self):
1677+
def test_pop_input(self):
16681678
input = """
1669-
inst(OP, (a, b, c --)) {
1670-
POP_DEAD_INPUTS();
1679+
inst(OP, (a, b --)) {
1680+
POP_INPUT(b);
16711681
HAM(a);
16721682
INPUTS_DEAD();
16731683
}
@@ -1678,8 +1688,10 @@ def test_pop_dead_inputs_some_live(self):
16781688
next_instr += 1;
16791689
INSTRUCTION_STATS(OP);
16801690
_PyStackRef a;
1681-
a = stack_pointer[-3];
1682-
stack_pointer += -2;
1691+
_PyStackRef b;
1692+
b = stack_pointer[-1];
1693+
a = stack_pointer[-2];
1694+
stack_pointer += -1;
16831695
assert(WITHIN_STACK_BOUNDS());
16841696
HAM(a);
16851697
stack_pointer += -1;
@@ -1689,29 +1701,23 @@ def test_pop_dead_inputs_some_live(self):
16891701
"""
16901702
self.run_cases_test(input, output)
16911703

1692-
def test_pop_dead_inputs_with_output(self):
1704+
def test_pop_input_with_empty_stack(self):
16931705
input = """
1694-
inst(OP, (a, b -- c)) {
1695-
POP_DEAD_INPUTS();
1696-
c = SPAM();
1706+
inst(OP, (--)) {
1707+
POP_INPUT(foo);
16971708
}
16981709
"""
1699-
output = """
1700-
TARGET(OP) {
1701-
frame->instr_ptr = next_instr;
1702-
next_instr += 1;
1703-
INSTRUCTION_STATS(OP);
1704-
_PyStackRef c;
1705-
stack_pointer += -2;
1706-
assert(WITHIN_STACK_BOUNDS());
1707-
c = SPAM();
1708-
stack_pointer[0] = c;
1709-
stack_pointer += 1;
1710-
assert(WITHIN_STACK_BOUNDS());
1711-
DISPATCH();
1710+
with self.assertRaises(SyntaxError):
1711+
self.run_cases_test(input, "")
1712+
1713+
def test_pop_input_with_non_tos(self):
1714+
input = """
1715+
inst(OP, (a, b --)) {
1716+
POP_INPUT(a);
17121717
}
17131718
"""
1714-
self.run_cases_test(input, output)
1719+
with self.assertRaises(SyntaxError):
1720+
self.run_cases_test(input, "")
17151721

17161722
def test_no_escaping_calls_in_branching_macros(self):
17171723

Lib/test/test_opcache.py

+80-10
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,16 @@ def instantiate():
564564
instantiate()
565565

566566

567+
def make_deferred_ref_count_obj():
568+
"""Create an object that uses deferred reference counting.
569+
570+
Only objects that use deferred refence counting may be stored in inline
571+
caches in free-threaded builds. This constructs a new class named Foo,
572+
which uses deferred reference counting.
573+
"""
574+
return type("Foo", (object,), {})
575+
576+
567577
@threading_helper.requires_working_threading()
568578
class TestRacesDoNotCrash(TestBase):
569579
# Careful with these. Bigger numbers have a higher chance of catching bugs,
@@ -714,11 +724,11 @@ def write(items):
714724
opname = "FOR_ITER_LIST"
715725
self.assert_races_do_not_crash(opname, get_items, read, write)
716726

717-
@requires_specialization
727+
@requires_specialization_ft
718728
def test_load_attr_class(self):
719729
def get_items():
720730
class C:
721-
a = object()
731+
a = make_deferred_ref_count_obj()
722732

723733
items = []
724734
for _ in range(self.ITEMS):
@@ -739,12 +749,45 @@ def write(items):
739749
del item.a
740750
except AttributeError:
741751
pass
742-
item.a = object()
752+
item.a = make_deferred_ref_count_obj()
743753

744754
opname = "LOAD_ATTR_CLASS"
745755
self.assert_races_do_not_crash(opname, get_items, read, write)
746756

747-
@requires_specialization
757+
@requires_specialization_ft
758+
def test_load_attr_class_with_metaclass_check(self):
759+
def get_items():
760+
class Meta(type):
761+
pass
762+
763+
class C(metaclass=Meta):
764+
a = make_deferred_ref_count_obj()
765+
766+
items = []
767+
for _ in range(self.ITEMS):
768+
item = C
769+
items.append(item)
770+
return items
771+
772+
def read(items):
773+
for item in items:
774+
try:
775+
item.a
776+
except AttributeError:
777+
pass
778+
779+
def write(items):
780+
for item in items:
781+
try:
782+
del item.a
783+
except AttributeError:
784+
pass
785+
item.a = make_deferred_ref_count_obj()
786+
787+
opname = "LOAD_ATTR_CLASS_WITH_METACLASS_CHECK"
788+
self.assert_races_do_not_crash(opname, get_items, read, write)
789+
790+
@requires_specialization_ft
748791
def test_load_attr_getattribute_overridden(self):
749792
def get_items():
750793
class C:
@@ -774,7 +817,7 @@ def write(items):
774817
opname = "LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN"
775818
self.assert_races_do_not_crash(opname, get_items, read, write)
776819

777-
@requires_specialization
820+
@requires_specialization_ft
778821
def test_load_attr_instance_value(self):
779822
def get_items():
780823
class C:
@@ -798,7 +841,7 @@ def write(items):
798841
opname = "LOAD_ATTR_INSTANCE_VALUE"
799842
self.assert_races_do_not_crash(opname, get_items, read, write)
800843

801-
@requires_specialization
844+
@requires_specialization_ft
802845
def test_load_attr_method_lazy_dict(self):
803846
def get_items():
804847
class C(Exception):
@@ -828,7 +871,7 @@ def write(items):
828871
opname = "LOAD_ATTR_METHOD_LAZY_DICT"
829872
self.assert_races_do_not_crash(opname, get_items, read, write)
830873

831-
@requires_specialization
874+
@requires_specialization_ft
832875
def test_load_attr_method_no_dict(self):
833876
def get_items():
834877
class C:
@@ -859,7 +902,7 @@ def write(items):
859902
opname = "LOAD_ATTR_METHOD_NO_DICT"
860903
self.assert_races_do_not_crash(opname, get_items, read, write)
861904

862-
@requires_specialization
905+
@requires_specialization_ft
863906
def test_load_attr_method_with_values(self):
864907
def get_items():
865908
class C:
@@ -914,7 +957,7 @@ def write(items):
914957
opname = "LOAD_ATTR_MODULE"
915958
self.assert_races_do_not_crash(opname, get_items, read, write)
916959

917-
@requires_specialization
960+
@requires_specialization_ft
918961
def test_load_attr_property(self):
919962
def get_items():
920963
class C:
@@ -944,7 +987,34 @@ def write(items):
944987
opname = "LOAD_ATTR_PROPERTY"
945988
self.assert_races_do_not_crash(opname, get_items, read, write)
946989

947-
@requires_specialization
990+
@requires_specialization_ft
991+
def test_load_attr_slot(self):
992+
def get_items():
993+
class C:
994+
__slots__ = ["a", "b"]
995+
996+
items = []
997+
for i in range(self.ITEMS):
998+
item = C()
999+
item.a = i
1000+
item.b = i + self.ITEMS
1001+
items.append(item)
1002+
return items
1003+
1004+
def read(items):
1005+
for item in items:
1006+
item.a
1007+
item.b
1008+
1009+
def write(items):
1010+
for item in items:
1011+
item.a = 100
1012+
item.b = 200
1013+
1014+
opname = "LOAD_ATTR_SLOT"
1015+
self.assert_races_do_not_crash(opname, get_items, read, write)
1016+
1017+
@requires_specialization_ft
9481018
def test_load_attr_with_hint(self):
9491019
def get_items():
9501020
class C:

0 commit comments

Comments
 (0)