diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index 60879e2774e7077..ba2c2e7df2f403c 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -38,6 +38,43 @@ def assert_no_opcode(self, f, opname): opnames = {instruction.opname for instruction in instructions} self.assertNotIn(opname, opnames) + def adaptive_counters(self, f): + """Map each specialized instruction in f to its adaptive counter.""" + counters = {} + for instruction in dis.get_instructions(f, adaptive=True): + if instruction.opname == instruction.baseopname: + continue + if instruction.baseopname in ("RESUME", "JUMP_BACKWARD"): + continue + cache = {name: data for name, _, data in instruction.cache_info} + if "counter" in cache: + counters[instruction.offset] = (instruction.opname, + cache["counter"]) + return counters + + def assert_specialization_stable(self, f, *args, calls=10): + """Assert that no specialized instruction in f misses its guard. + + A guard miss advances the instruction's adaptive counter. f should + have a fresh code object (see reset_code()), otherwise leftover + specializations from earlier runs can show up as misses. + """ + before = self.adaptive_counters(f) + self.assertTrue(before, f"{f.__qualname__} has no specialized " + "instructions") + for _ in range(calls): + f(*args) + after = self.adaptive_counters(f) + # Ignore instructions that only specialize during these calls. + moved = [] + for off, (op, counter) in before.items(): + if after.get(off) != (op, counter): + now = after[off][1] if off in after else "unspecialized" + moved.append(f"{op} at offset {off}: counter {counter} -> {now}") + self.assertEqual(moved, [], + f"specialized instructions in {f.__qualname__} " + f"missed their guard during {calls} calls") + class TestLoadSuperAttrCache(unittest.TestCase): def test_descriptor_not_double_executed_on_spec_fail(self): @@ -2156,6 +2193,7 @@ def list_append(l): for _ in r: l.append(1) + reset_code(list_append) list_append([]) self.assert_specialized(list_append, "CALL_LIST_APPEND") self.assert_no_opcode(list_append, "CALL_METHOD_DESCRIPTOR_O") @@ -2166,10 +2204,45 @@ def my_list_append(l): l.append(1) class MyList(list): pass - my_list_append(MyList()) + my_list = MyList() + reset_code(my_list_append) + my_list_append(my_list) self.assert_specialized(my_list_append, "CALL_METHOD_DESCRIPTOR_O") self.assert_no_opcode(my_list_append, "CALL_LIST_APPEND") self.assert_no_opcode(my_list_append, "CALL") + self.assert_specialization_stable(my_list_append, my_list) + + @cpython_only + @requires_specialization + @requires_jit_disabled + def test_call_method_descriptor_subclass_instance(self): + # C methods inherited from a built-in type must not miss + # their guard on subclass instances. One case per guard, plus unbound. + class MyStr(str): pass + Point = collections.namedtuple("Point", "x y") + counts = collections.defaultdict(int, {1: 2}) + + def noargs(s): return s.upper() + def o(p): return p.count(1) + def fast(d): return d.get(1) + def fast_with_keywords(s): return s.split() + def unbound(d): return dict.get(d, 1) + + cases = [ + (noargs, MyStr("abc"), "CALL_METHOD_DESCRIPTOR_NOARGS"), + (o, Point(1, 2), "CALL_METHOD_DESCRIPTOR_O"), + (fast, counts, "CALL_METHOD_DESCRIPTOR_FAST"), + (fast_with_keywords, MyStr("a b"), + "CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS"), + (unbound, counts, "CALL_METHOD_DESCRIPTOR_FAST"), + ] + for f, obj, opname in cases: + with self.subTest(f=f.__name__, obj=type(obj).__name__): + reset_code(f) + for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD): + f(obj) + self.assert_specialized(f, opname) + self.assert_specialization_stable(f, obj) @cpython_only @requires_specialization diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-08-45-17.gh-issue-158639.yHi2F2.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-08-45-17.gh-issue-158639.yHi2F2.rst new file mode 100644 index 000000000000000..55890fff31ead9f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-08-45-17.gh-issue-158639.yHi2F2.rst @@ -0,0 +1,5 @@ +Fix specialization of calls to C methods inherited from built-in types when +the receiver is an instance of a subclass, such as +:class:`~collections.defaultdict` or a :func:`~collections.namedtuple`. The +specialized ``CALL_METHOD_DESCRIPTOR_*`` instructions previously failed their +guard on every such call and fell back to the generic call path. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 3bdc16437e2bc61..344a97127350614 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -4061,7 +4061,7 @@ } PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UPDATE_MISS_STATS(CALL); assert(_PyOpcode_Deopt[opcode] == (CALL)); JUMP_TO_PREDICTED(CALL); @@ -4182,7 +4182,7 @@ JUMP_TO_PREDICTED(CALL); } PyObject *self = PyStackRef_AsPyObjectBorrow(arguments[0]); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UPDATE_MISS_STATS(CALL); assert(_PyOpcode_Deopt[opcode] == (CALL)); JUMP_TO_PREDICTED(CALL); @@ -4305,7 +4305,7 @@ } PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UPDATE_MISS_STATS(CALL); assert(_PyOpcode_Deopt[opcode] == (CALL)); JUMP_TO_PREDICTED(CALL); @@ -4429,7 +4429,7 @@ } PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UPDATE_MISS_STATS(CALL); assert(_PyOpcode_Deopt[opcode] == (CALL)); JUMP_TO_PREDICTED(CALL); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 31eaeab0d67841f..3205e1333cda16f 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -5060,7 +5060,7 @@ dummy_func( EXIT_IF(total_args != 2); PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - EXIT_IF(!Py_IS_TYPE(self, method->d_common.d_type)); + EXIT_IF(!PyObject_TypeCheck(self, method->d_common.d_type)); } op(_CALL_METHOD_DESCRIPTOR_O, (callable, self_or_null, args[oparg] -- res, c, s, a)) { @@ -5137,7 +5137,7 @@ dummy_func( } EXIT_IF(total_args == 0); PyObject *self = PyStackRef_AsPyObjectBorrow(arguments[0]); - EXIT_IF(!Py_IS_TYPE(self, method->d_common.d_type)); + EXIT_IF(!PyObject_TypeCheck(self, method->d_common.d_type)); } op(_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS, (callable, self_or_null, args[oparg] -- callable, self_or_null, args[oparg])) { @@ -5211,7 +5211,7 @@ dummy_func( EXIT_IF(total_args != 1); PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - EXIT_IF(!Py_IS_TYPE(self, method->d_common.d_type)); + EXIT_IF(!PyObject_TypeCheck(self, method->d_common.d_type)); } op(_CALL_METHOD_DESCRIPTOR_NOARGS, (callable, self_or_null, args[oparg] -- res, c, s)) { @@ -5281,7 +5281,7 @@ dummy_func( EXIT_IF(total_args == 0); PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - EXIT_IF(!Py_IS_TYPE(self, method->d_common.d_type)); + EXIT_IF(!PyObject_TypeCheck(self, method->d_common.d_type)); } op(_CALL_METHOD_DESCRIPTOR_FAST, (callable, self_or_null, args[oparg] -- callable, self_or_null, args[oparg])) { diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index c5b2dfcf5f618f3..ef7cb4ae0470cd4 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -19272,7 +19272,7 @@ } PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UOP_STAT_INC(uopcode, miss); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_JUMP_TARGET(); @@ -19479,7 +19479,7 @@ JUMP_TO_JUMP_TARGET(); } PyObject *self = PyStackRef_AsPyObjectBorrow(arguments[0]); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UOP_STAT_INC(uopcode, miss); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_JUMP_TARGET(); @@ -19623,7 +19623,7 @@ } PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UOP_STAT_INC(uopcode, miss); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_JUMP_TARGET(); @@ -19753,7 +19753,7 @@ } PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UOP_STAT_INC(uopcode, miss); SET_CURRENT_CACHED_VALUES(0); JUMP_TO_JUMP_TARGET(); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index dd0ce41e4b06b4a..9487417ed6f8cdc 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -4061,7 +4061,7 @@ } PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UPDATE_MISS_STATS(CALL); assert(_PyOpcode_Deopt[opcode] == (CALL)); JUMP_TO_PREDICTED(CALL); @@ -4182,7 +4182,7 @@ JUMP_TO_PREDICTED(CALL); } PyObject *self = PyStackRef_AsPyObjectBorrow(arguments[0]); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UPDATE_MISS_STATS(CALL); assert(_PyOpcode_Deopt[opcode] == (CALL)); JUMP_TO_PREDICTED(CALL); @@ -4305,7 +4305,7 @@ } PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UPDATE_MISS_STATS(CALL); assert(_PyOpcode_Deopt[opcode] == (CALL)); JUMP_TO_PREDICTED(CALL); @@ -4429,7 +4429,7 @@ } PyObject *self = PyStackRef_AsPyObjectBorrow( PyStackRef_IsNull(self_or_null) ? args[0] : self_or_null); - if (!Py_IS_TYPE(self, method->d_common.d_type)) { + if (!PyObject_TypeCheck(self, method->d_common.d_type)) { UPDATE_MISS_STATS(CALL); assert(_PyOpcode_Deopt[opcode] == (CALL)); JUMP_TO_PREDICTED(CALL);