diff --git a/Include/internal/pycore_optimizer.h b/Include/internal/pycore_optimizer.h index 8c35c4416fe3c8c..25e04171f4d5189 100644 --- a/Include/internal/pycore_optimizer.h +++ b/Include/internal/pycore_optimizer.h @@ -206,7 +206,7 @@ typedef struct _PyExecutorObject { PyAPI_FUNC(_PyExecutorObject*) _Py_GetExecutor(PyCodeObject *code, int offset); int _Py_ExecutorInit(_PyExecutorObject *, const _PyBloomFilter *); -void _Py_ExecutorDetach(_PyExecutorObject *); +PyAPI_FUNC(void) _Py_ExecutorDetach(_PyExecutorObject *); PyAPI_FUNC(void) _Py_Executor_DependsOn(_PyExecutorObject *executor, void *obj); /* We use a bloomfilter with k = 6, m = 256 diff --git a/Include/internal/pycore_uop_metadata.h b/Include/internal/pycore_uop_metadata.h index 6713e9bc95f942d..55b7f5160201683 100644 --- a/Include/internal/pycore_uop_metadata.h +++ b/Include/internal/pycore_uop_metadata.h @@ -413,7 +413,7 @@ const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = { [_ERROR_POP_N] = HAS_ARG_FLAG | HAS_SYNC_SP_FLAG, [_SPILL_OR_RELOAD] = 0, [_TIER2_RESUME_CHECK] = HAS_PERIODIC_FLAG, - [_COLD_EXIT] = HAS_SYNC_SP_FLAG, + [_COLD_EXIT] = HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG, [_COLD_DYNAMIC_EXIT] = HAS_SYNC_SP_FLAG, [_GUARD_CODE_VERSION__PUSH_FRAME] = HAS_EXIT_FLAG, [_GUARD_CODE_VERSION_YIELD_VALUE] = HAS_EXIT_FLAG, diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index b2439dfdeb7a221..06271a0e2732754 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -5,13 +5,14 @@ import unittest import gc import os +import subprocess import types import _opcode from test.support import (script_helper, requires_specialization, import_helper, Py_GIL_DISABLED, requires_jit_enabled, - reset_code) + reset_code, SHORT_TIMEOUT) _testinternalcapi = import_helper.import_module("_testinternalcapi") @@ -6200,6 +6201,31 @@ def __exit__(self, e, v, t): ... f1() """), PYTHON_JIT="1") + def test_for_iter_side_exit_does_not_self_link(self): + subprocess.run([sys.executable, "-c", textwrap.dedent(""" + from _testinternalcapi import TIER2_THRESHOLD + + def exhaust(iterator): + for _ in iterator: + pass + + values = range(TIER2_THRESHOLD) + # After the initial trace, MAX_CHAIN_DEPTH side exits cause the final + # executor to be installed at FOR_ITER. + warmup_iterators = ( + iter(set(values)), + iter(dict.fromkeys(values)), + iter(values), + enumerate(values), + zip(values, values), + ) + for iterator in warmup_iterators: + exhaust(iterator) + + # A different iterator type must not link that executor to itself. + exhaust(map(bool, values)) + """)], check=True, timeout=SHORT_TIMEOUT) + def global_identity(x): return x diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-10-14-56.gh-issue-154701.zulh2S.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-10-14-56.gh-issue-154701.zulh2S.rst new file mode 100644 index 000000000000000..c32803e71cbf993 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-10-14-56.gh-issue-154701.zulh2S.rst @@ -0,0 +1 @@ +Fix an infinite loop in JIT when a ``FOR_ITER`` side exit links an executor back to itself. diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 4f0d0227cc2eb04..f841e12793a8001 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -6308,6 +6308,10 @@ dummy_func( if (target->op.code == ENTER_EXECUTOR) { PyCodeObject *code = _PyFrame_GetCode(frame); executor = code->co_executors->executors[target->op.arg]; + if (executor == _PyExecutor_FromExit(exit)) { + _Py_ExecutorDetach(executor); + GOTO_TIER_ONE(target); + } Py_INCREF(executor); assert(tstate->jit_exit == exit); exit->executor = executor; diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 7644c2d38d7f068..dc5c68794e8a5f5 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -23676,6 +23676,13 @@ if (target->op.code == ENTER_EXECUTOR) { PyCodeObject *code = _PyFrame_GetCode(frame); executor = code->co_executors->executors[target->op.arg]; + if (executor == _PyExecutor_FromExit(exit)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _Py_ExecutorDetach(executor); + stack_pointer = _PyFrame_GetStackPointer(frame); + SET_CURRENT_CACHED_VALUES(0); + GOTO_TIER_ONE(target); + } Py_INCREF(executor); assert(tstate->jit_exit == exit); exit->executor = executor;