From d9a51152654527ca74521a59ce70785ed2c70181 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 2 May 2026 10:38:05 +0300 Subject: [PATCH 1/2] gh-149239: Deopt `LOAD_ATTR_CLASS_WITH_METACLASS_CHECK` on reassining `__class__` --- .jit-stamp | 0 Include/internal/pycore_uop_metadata.h | 2 +- Lib/test/test_capi/test_opt.py | 45 +++++++++++++++++++ ...-05-02-10-37-24.gh-issue-149239.lMoium.rst | 2 + Modules/_testinternalcapi/test_cases.c.h | 16 +++++++ Python/bytecodes.c | 5 +++ Python/executor_cases.c.h | 9 ++++ Python/generated_cases.c.h | 16 +++++++ 8 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 .jit-stamp create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-05-02-10-37-24.gh-issue-149239.lMoium.rst diff --git a/.jit-stamp b/.jit-stamp new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/Include/internal/pycore_uop_metadata.h b/Include/internal/pycore_uop_metadata.h index 8f543dbeeb8bc9..6159a7dfef9486 100644 --- a/Include/internal/pycore_uop_metadata.h +++ b/Include/internal/pycore_uop_metadata.h @@ -227,7 +227,7 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = { [_LOAD_ATTR_WITH_HINT] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_EXIT_FLAG, [_LOAD_ATTR_SLOT] = HAS_EXIT_FLAG, [_CHECK_ATTR_CLASS] = HAS_EXIT_FLAG, - [_LOAD_ATTR_CLASS] = HAS_ESCAPES_FLAG, + [_LOAD_ATTR_CLASS] = HAS_EXIT_FLAG | HAS_ESCAPES_FLAG, [_LOAD_ATTR_PROPERTY_FRAME] = HAS_ARG_FLAG | HAS_EXIT_FLAG, [_LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_EXIT_FLAG, [_GUARD_DORV_NO_DICT] = HAS_EXIT_FLAG, diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 7118dfeed9faee..f7a078d69ae2a9 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -1,4 +1,5 @@ import contextlib +import enum import itertools import sys import textwrap @@ -3511,6 +3512,50 @@ def f(n): self.assertNotIn("_LOAD_ATTR_METHOD_NO_DICT", uops) self.assertIn("_LOAD_CONST_INLINE_BORROW", uops) + def test_load_attr_class_with_metaclass_check(self): + # LOAD_ATTR_CLASS_WITH_METACLASS_CHECK must check + # for `__class__` writes, see gh-149239 + class ColorMeta(enum.EnumType): + pass + + class Color(enum.IntEnum, metaclass=ColorMeta): + RED = 1 + + red = Color.RED + + def f1(n): + for _ in range(n): + assert Color.RED == 1 + return n + + res, ex = self._run_with_optimizer(f1, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + self.assertEqual(res, TIER2_THRESHOLD) + uops = get_opnames(ex) + self.assertIn("_CHECK_ATTR_CLASS", uops) + self.assertIn("_GUARD_TYPE_VERSION", uops) + + # Reassign the `__class__` attr to deopt: + class Descriptor(enum.IntEnum): + RED = 1 + + def __get__(self, obj, owner): + return "descr" + + red.__class__ = Descriptor + + def f2(n): + for _ in range(n): + assert Color.RED == 'descr' + return n + + res, ex = self._run_with_optimizer(f2, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + self.assertEqual(res, TIER2_THRESHOLD) + uops = get_opnames(ex) + self.assertNotIn("_CHECK_ATTR_CLASS", uops) + self.assertNotIn("_GUARD_TYPE_VERSION", uops) + def test_cached_load_special(self): class CM: def __enter__(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-02-10-37-24.gh-issue-149239.lMoium.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-02-10-37-24.gh-issue-149239.lMoium.rst new file mode 100644 index 00000000000000..99d038de9d21f3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-02-10-37-24.gh-issue-149239.lMoium.rst @@ -0,0 +1,2 @@ +Deopt ``LOAD_ATTR_CLASS_WITH_METACLASS_CHECK`` opcode on ``__class__`` +reassigning. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 8897854078bd45..0c72f0d7a780d1 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -8352,6 +8352,14 @@ // _LOAD_ATTR_CLASS { PyObject *descr = read_obj(&this_instr[6].cache); + PyTypeObject *descr_type = Py_TYPE(descr); + PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner); + if ((descr_type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0 + && descr_type != (PyTypeObject *)owner_o) { + UPDATE_MISS_STATS(LOAD_ATTR); + assert(_PyOpcode_Deopt[opcode] == (LOAD_ATTR)); + JUMP_TO_PREDICTED(LOAD_ATTR); + } STAT_INC(LOAD_ATTR, hit); assert(descr != NULL); attr = PyStackRef_FromPyObjectNew(descr); @@ -8420,6 +8428,14 @@ // _LOAD_ATTR_CLASS { PyObject *descr = read_obj(&this_instr[6].cache); + PyTypeObject *descr_type = Py_TYPE(descr); + PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner); + if ((descr_type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0 + && descr_type != (PyTypeObject *)owner_o) { + UPDATE_MISS_STATS(LOAD_ATTR); + assert(_PyOpcode_Deopt[opcode] == (LOAD_ATTR)); + JUMP_TO_PREDICTED(LOAD_ATTR); + } STAT_INC(LOAD_ATTR, hit); assert(descr != NULL); attr = PyStackRef_FromPyObjectNew(descr); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index d485172c82fa0d..892f48396407ab 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2976,6 +2976,11 @@ dummy_func( } op(_LOAD_ATTR_CLASS, (descr/4, owner -- attr)) { + PyTypeObject *descr_type = Py_TYPE(descr); + PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner); + EXIT_IF((descr_type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0 + && descr_type != (PyTypeObject *)owner_o); + STAT_INC(LOAD_ATTR, hit); assert(descr != NULL); attr = PyStackRef_FromPyObjectNew(descr); diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index f8fc35de9d7957..6b2457e436aeab 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -11967,6 +11967,15 @@ _PyStackRef _stack_item_0 = _tos_cache0; owner = _stack_item_0; PyObject *descr = (PyObject *)CURRENT_OPERAND0_64(); + PyTypeObject *descr_type = Py_TYPE(descr); + PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner); + if ((descr_type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0 + && descr_type != (PyTypeObject *)owner_o) { + UOP_STAT_INC(uopcode, miss); + _tos_cache0 = owner; + SET_CURRENT_CACHED_VALUES(1); + JUMP_TO_JUMP_TARGET(); + } STAT_INC(LOAD_ATTR, hit); assert(descr != NULL); attr = PyStackRef_FromPyObjectNew(descr); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index dccee0e4a3b110..a84fbff2be61a8 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -8351,6 +8351,14 @@ // _LOAD_ATTR_CLASS { PyObject *descr = read_obj(&this_instr[6].cache); + PyTypeObject *descr_type = Py_TYPE(descr); + PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner); + if ((descr_type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0 + && descr_type != (PyTypeObject *)owner_o) { + UPDATE_MISS_STATS(LOAD_ATTR); + assert(_PyOpcode_Deopt[opcode] == (LOAD_ATTR)); + JUMP_TO_PREDICTED(LOAD_ATTR); + } STAT_INC(LOAD_ATTR, hit); assert(descr != NULL); attr = PyStackRef_FromPyObjectNew(descr); @@ -8419,6 +8427,14 @@ // _LOAD_ATTR_CLASS { PyObject *descr = read_obj(&this_instr[6].cache); + PyTypeObject *descr_type = Py_TYPE(descr); + PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner); + if ((descr_type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE) == 0 + && descr_type != (PyTypeObject *)owner_o) { + UPDATE_MISS_STATS(LOAD_ATTR); + assert(_PyOpcode_Deopt[opcode] == (LOAD_ATTR)); + JUMP_TO_PREDICTED(LOAD_ATTR); + } STAT_INC(LOAD_ATTR, hit); assert(descr != NULL); attr = PyStackRef_FromPyObjectNew(descr); From 49aa0101abf91f7e6f66aece6b7ef37b15ee49a0 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 3 May 2026 10:17:20 +0300 Subject: [PATCH 2/2] Address review --- Lib/test/test_capi/test_opt.py | 45 ---------------------------------- Lib/test/test_opcache.py | 40 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 45 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index f7a078d69ae2a9..7118dfeed9faee 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -1,5 +1,4 @@ import contextlib -import enum import itertools import sys import textwrap @@ -3512,50 +3511,6 @@ def f(n): self.assertNotIn("_LOAD_ATTR_METHOD_NO_DICT", uops) self.assertIn("_LOAD_CONST_INLINE_BORROW", uops) - def test_load_attr_class_with_metaclass_check(self): - # LOAD_ATTR_CLASS_WITH_METACLASS_CHECK must check - # for `__class__` writes, see gh-149239 - class ColorMeta(enum.EnumType): - pass - - class Color(enum.IntEnum, metaclass=ColorMeta): - RED = 1 - - red = Color.RED - - def f1(n): - for _ in range(n): - assert Color.RED == 1 - return n - - res, ex = self._run_with_optimizer(f1, TIER2_THRESHOLD) - self.assertIsNotNone(ex) - self.assertEqual(res, TIER2_THRESHOLD) - uops = get_opnames(ex) - self.assertIn("_CHECK_ATTR_CLASS", uops) - self.assertIn("_GUARD_TYPE_VERSION", uops) - - # Reassign the `__class__` attr to deopt: - class Descriptor(enum.IntEnum): - RED = 1 - - def __get__(self, obj, owner): - return "descr" - - red.__class__ = Descriptor - - def f2(n): - for _ in range(n): - assert Color.RED == 'descr' - return n - - res, ex = self._run_with_optimizer(f2, TIER2_THRESHOLD) - self.assertIsNotNone(ex) - self.assertEqual(res, TIER2_THRESHOLD) - uops = get_opnames(ex) - self.assertNotIn("_CHECK_ATTR_CLASS", uops) - self.assertNotIn("_GUARD_TYPE_VERSION", uops) - def test_cached_load_special(self): class CM: def __enter__(self): diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index f5f408fcb4b311..9751ad7cea358b 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -1,3 +1,4 @@ +import enum import copy import pickle import dis @@ -2114,6 +2115,45 @@ def load_enum_member(): self.assert_specialized(load_enum_member, "LOAD_ATTR_CLASS_WITH_METACLASS_CHECK") + @cpython_only + @requires_specialization + def test_load_attr_class_with_metaclass_check_149239(self): + # LOAD_ATTR_CLASS_WITH_METACLASS_CHECK must check + # for `__class__` writes, see gh-149239 + class ColorMeta(enum.EnumType): + pass + + class Color(enum.IntEnum, metaclass=ColorMeta): + RED = 1 + + red = Color.RED + + def f1(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + assert Color.RED == 1 + + f1() + self.assert_specialized(f1, + "LOAD_ATTR_CLASS_WITH_METACLASS_CHECK") + + # Reassign the `__class__` attr to deopt: + class Descriptor(enum.IntEnum): + RED = 1 + + def __get__(self, obj, owner): + return "descr" + + red.__class__ = Descriptor + + def f2(): + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + assert Color.RED == 'descr' + + f2() + self.assert_no_opcode(f2, + "LOAD_ATTR_CLASS_WITH_METACLASS_CHECK") + + if __name__ == "__main__": unittest.main()