diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index fbfd5e1e75b766..a5c4ffa38fe80a 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -1282,6 +1282,64 @@ always available. Unless explicitly noted otherwise, all variables are read-only .. versionadded:: 3.5 +.. data:: _jit + + Utilities for observing just-in-time compilation. + + .. impl-detail:: + + JIT compilation is an *experimental implementation detail* of CPython. + ``sys._jit`` is not guaranteed to exist or behave the same way in all + Python implementations, versions, or build configurations. + + .. versionadded:: next + + .. function:: _jit.is_available() + + Return ``True`` if the current Python executable supports JIT compilation, + and ``False`` otherwise. This can be controlled by building CPython with + the ``--experimental-jit`` option on Windows, and the + :option:`--enable-experimental-jit` option on all other platforms. + + .. function:: _jit.is_enabled() + + Return ``True`` if JIT compilation is enabled for the current Python + process (implies :func:`sys._jit.is_available`), and ``False`` otherwise. + If JIT compilation is available, this can be controlled by setting the + :envvar:`PYTHON_JIT` environment variable to ``0`` (disabled) or ``1`` + (enabled) at interpreter startup. + + .. function:: _jit.is_active() + + Return ``True`` if the topmost Python frame is currently executing JIT + code (implies :func:`sys._jit.is_enabled`), and ``False`` otherwise. + + .. note:: + + This function is intended for testing and debugging the JIT itself. + It should be avoided for any other purpose. + + .. note:: + + Due to the nature of tracing JIT compilers, repeated calls to this + function may give surprising results. For example, branching on its + return value will likely lead to unexpected behavior (if doing so + causes JIT code to be entered or exited): + + .. code-block:: pycon + + >>> for warmup in range(BIG_NUMBER): + ... # This line is "hot", and is eventually JIT-compiled: + ... if sys._jit.is_active(): + ... # This line is "cold", and is run in the interpreter: + ... assert sys._jit.is_active() + ... + Traceback (most recent call last): + File "", line 5, in + assert sys._jit.is_active() + ~~~~~~~~~~~~~~~~~~^^ + AssertionError + .. data:: last_exc This variable is not always defined; it is set to the exception instance diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index a4889cfc36440f..c586882b313ddf 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -1279,6 +1279,14 @@ conflict. .. versionadded:: 3.14 +.. envvar:: PYTHON_JIT + + On builds where experimental just-in-time compilation is available, this + variable can force the JIT to be disabled (``0``) or enabled (``1``) at + interpreter startup. + + .. versionadded:: 3.13 + Debug-mode variables ~~~~~~~~~~~~~~~~~~~~ diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py index c4a1506c9a7d60..63a2e427d185f1 100644 --- a/Lib/test/libregrtest/utils.py +++ b/Lib/test/libregrtest/utils.py @@ -335,43 +335,11 @@ def get_build_info(): build.append('with_assert') # --enable-experimental-jit - tier2 = re.search('-D_Py_TIER2=([0-9]+)', cflags) - if tier2: - tier2 = int(tier2.group(1)) - - if not sys.flags.ignore_environment: - PYTHON_JIT = os.environ.get('PYTHON_JIT', None) - if PYTHON_JIT: - PYTHON_JIT = (PYTHON_JIT != '0') - else: - PYTHON_JIT = None - - if tier2 == 1: # =yes - if PYTHON_JIT == False: - jit = 'JIT=off' - else: - jit = 'JIT' - elif tier2 == 3: # =yes-off - if PYTHON_JIT: - jit = 'JIT' + if sys._jit.is_available(): + if sys._jit.is_enabled(): + build.append("JIT") else: - jit = 'JIT=off' - elif tier2 == 4: # =interpreter - if PYTHON_JIT == False: - jit = 'JIT-interpreter=off' - else: - jit = 'JIT-interpreter' - elif tier2 == 6: # =interpreter-off (Secret option!) - if PYTHON_JIT: - jit = 'JIT-interpreter' - else: - jit = 'JIT-interpreter=off' - elif '-D_Py_JIT' in cflags: - jit = 'JIT' - else: - jit = None - if jit: - build.append(jit) + build.append("JIT (disabled)") # --enable-framework=name framework = sysconfig.get_config_var('PYTHONFRAMEWORK') diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 23582c58c0a00b..041f1250003b68 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2648,13 +2648,9 @@ def exceeds_recursion_limit(): Py_TRACE_REFS = hasattr(sys, 'getobjects') -try: - from _testinternalcapi import jit_enabled -except ImportError: - requires_jit_enabled = requires_jit_disabled = unittest.skip("requires _testinternalcapi") -else: - requires_jit_enabled = unittest.skipUnless(jit_enabled(), "requires JIT enabled") - requires_jit_disabled = unittest.skipIf(jit_enabled(), "requires JIT disabled") +_JIT_ENABLED = sys._jit.is_enabled() +requires_jit_enabled = unittest.skipUnless(_JIT_ENABLED, "requires JIT enabled") +requires_jit_disabled = unittest.skipIf(_JIT_ENABLED, "requires JIT disabled") _BASE_COPY_SRC_DIR_IGNORED_NAMES = frozenset({ diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 98dc3b42ef0bec..a597f23a992e7b 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -306,7 +306,7 @@ def test_getitem_with_error(self): CURRENT_THREAD_REGEX + r' File .*, line 6 in \n' r'\n' - r'Extension modules: _testcapi, _testinternalcapi \(total: 2\)\n') + r'Extension modules: _testcapi \(total: 1\)\n') else: # Python built with NDEBUG macro defined: # test _Py_CheckFunctionResult() instead. diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index f2586fcee57d87..ae68c1dd75c641 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -1336,7 +1336,7 @@ def test_loop_quicken(self): # Loop can trigger a quicken where the loop is located self.code_quicken(loop_test) got = self.get_disassembly(loop_test, adaptive=True) - jit = import_helper.import_module("_testinternalcapi").jit_enabled() + jit = sys._jit.is_enabled() expected = dis_loop_test_quickened_code.format("JIT" if jit else "NO_JIT") self.do_disassembly_compare(got, expected) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index aeca3720cfaedc..bcc7b4de048398 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -2196,6 +2196,64 @@ def test_remote_exec_in_process_without_debug_fails_xoption(self): self.assertIn(b"Remote debugging is not enabled", err) self.assertEqual(out, b"") +class TestSysJIT(unittest.TestCase): + + def test_jit_is_available(self): + available = sys._jit.is_available() + script = f"import sys; assert sys._jit.is_available() is {available}" + assert_python_ok("-c", script, PYTHON_JIT="0") + assert_python_ok("-c", script, PYTHON_JIT="1") + + def test_jit_is_enabled(self): + available = sys._jit.is_available() + script = "import sys; assert sys._jit.is_enabled() is {enabled}" + assert_python_ok("-c", script.format(enabled=False), PYTHON_JIT="0") + assert_python_ok("-c", script.format(enabled=available), PYTHON_JIT="1") + + def test_jit_is_active(self): + available = sys._jit.is_available() + script = textwrap.dedent( + """ + import _testcapi + import _testinternalcapi + import sys + + def frame_0_interpreter() -> None: + assert sys._jit.is_active() is False + + def frame_1_interpreter() -> None: + assert sys._jit.is_active() is False + frame_0_interpreter() + assert sys._jit.is_active() is False + + def frame_2_jit(expected: bool) -> None: + # Inlined into the last loop of frame_3_jit: + assert sys._jit.is_active() is expected + # Insert C frame: + _testcapi.pyobject_vectorcall(frame_1_interpreter, None, None) + assert sys._jit.is_active() is expected + + def frame_3_jit() -> None: + # JITs just before the last loop: + for i in range(_testinternalcapi.TIER2_THRESHOLD + 1): + # Careful, doing this in the reverse order breaks tracing: + expected = {enabled} and i == _testinternalcapi.TIER2_THRESHOLD + assert sys._jit.is_active() is expected + frame_2_jit(expected) + assert sys._jit.is_active() is expected + + def frame_4_interpreter() -> None: + assert sys._jit.is_active() is False + frame_3_jit() + assert sys._jit.is_active() is False + + assert sys._jit.is_active() is False + frame_4_interpreter() + assert sys._jit.is_active() is False + """ + ) + assert_python_ok("-c", script.format(enabled=False), PYTHON_JIT="0") + assert_python_ok("-c", script.format(enabled=available), PYTHON_JIT="1") if __name__ == "__main__": diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-30-19-07-11.gh-issue-133231.H9T8g_.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-30-19-07-11.gh-issue-133231.H9T8g_.rst new file mode 100644 index 00000000000000..7892ff25f2e363 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-30-19-07-11.gh-issue-133231.H9T8g_.rst @@ -0,0 +1,3 @@ +Add new utilities of observing JIT compilation: +:func:`sys._jit.is_available`, :func:`sys._jit.is_enabled`, and +:func:`sys._jit.is_active`. diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 0fbeb9928c82e2..e7f4510b714af3 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -1206,13 +1206,6 @@ verify_stateless_code(PyObject *self, PyObject *args, PyObject *kwargs) Py_RETURN_NONE; } - -static PyObject * -jit_enabled(PyObject *self, PyObject *arg) -{ - return PyBool_FromLong(_PyInterpreterState_GET()->jit); -} - #ifdef _Py_TIER2 static PyObject * @@ -2337,7 +2330,6 @@ static PyMethodDef module_functions[] = { METH_VARARGS | METH_KEYWORDS, NULL}, {"verify_stateless_code", _PyCFunction_CAST(verify_stateless_code), METH_VARARGS | METH_KEYWORDS, NULL}, - {"jit_enabled", jit_enabled, METH_NOARGS, NULL}, #ifdef _Py_TIER2 {"add_executor_dependency", add_executor_dependency, METH_VARARGS, NULL}, {"invalidate_executors", invalidate_executors, METH_O, NULL}, diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index 8b73ccefc30ee5..a47e4d11b54441 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -1821,6 +1821,90 @@ sys__is_gil_enabled(PyObject *module, PyObject *Py_UNUSED(ignored)) return return_value; } +PyDoc_STRVAR(_jit_is_available__doc__, +"is_available($module, /)\n" +"--\n" +"\n" +"Return True if the current Python executable supports JIT compilation, and False otherwise."); + +#define _JIT_IS_AVAILABLE_METHODDEF \ + {"is_available", (PyCFunction)_jit_is_available, METH_NOARGS, _jit_is_available__doc__}, + +static int +_jit_is_available_impl(PyObject *module); + +static PyObject * +_jit_is_available(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyObject *return_value = NULL; + int _return_value; + + _return_value = _jit_is_available_impl(module); + if ((_return_value == -1) && PyErr_Occurred()) { + goto exit; + } + return_value = PyBool_FromLong((long)_return_value); + +exit: + return return_value; +} + +PyDoc_STRVAR(_jit_is_enabled__doc__, +"is_enabled($module, /)\n" +"--\n" +"\n" +"Return True if JIT compilation is enabled for the current Python process (implies sys._jit.is_available()), and False otherwise."); + +#define _JIT_IS_ENABLED_METHODDEF \ + {"is_enabled", (PyCFunction)_jit_is_enabled, METH_NOARGS, _jit_is_enabled__doc__}, + +static int +_jit_is_enabled_impl(PyObject *module); + +static PyObject * +_jit_is_enabled(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyObject *return_value = NULL; + int _return_value; + + _return_value = _jit_is_enabled_impl(module); + if ((_return_value == -1) && PyErr_Occurred()) { + goto exit; + } + return_value = PyBool_FromLong((long)_return_value); + +exit: + return return_value; +} + +PyDoc_STRVAR(_jit_is_active__doc__, +"is_active($module, /)\n" +"--\n" +"\n" +"Return True if the topmost Python frame is currently executing JIT code (implies sys._jit.is_enabled()), and False otherwise."); + +#define _JIT_IS_ACTIVE_METHODDEF \ + {"is_active", (PyCFunction)_jit_is_active, METH_NOARGS, _jit_is_active__doc__}, + +static int +_jit_is_active_impl(PyObject *module); + +static PyObject * +_jit_is_active(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyObject *return_value = NULL; + int _return_value; + + _return_value = _jit_is_active_impl(module); + if ((_return_value == -1) && PyErr_Occurred()) { + goto exit; + } + return_value = PyBool_FromLong((long)_return_value); + +exit: + return return_value; +} + #ifndef SYS_GETWINDOWSVERSION_METHODDEF #define SYS_GETWINDOWSVERSION_METHODDEF #endif /* !defined(SYS_GETWINDOWSVERSION_METHODDEF) */ @@ -1864,4 +1948,4 @@ sys__is_gil_enabled(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=1aca52cefbeb800f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=449d16326e69dcf6 input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index e650444108e8f7..00dce4527fbb90 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -3986,6 +3986,71 @@ _PySys_SetPreliminaryStderr(PyObject *sysdict) PyObject *_Py_CreateMonitoringObject(void); +/*[clinic input] +module _jit +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=10952f74d7bbd972]*/ + +PyDoc_STRVAR(_jit_doc, "Utilities for observing just-in-time compilation."); + +/*[clinic input] +_jit.is_available -> bool +Return True if the current Python executable supports JIT compilation, and False otherwise. +[clinic start generated code]*/ + +static int +_jit_is_available_impl(PyObject *module) +/*[clinic end generated code: output=6849a9cd2ff4aac9 input=03add84aa8347cf1]*/ +{ + (void)module; +#ifdef _Py_TIER2 + return true; +#else + return false; +#endif +} + +/*[clinic input] +_jit.is_enabled -> bool +Return True if JIT compilation is enabled for the current Python process (implies sys._jit.is_available()), and False otherwise. +[clinic start generated code]*/ + +static int +_jit_is_enabled_impl(PyObject *module) +/*[clinic end generated code: output=55865f8de993fe42 input=02439394da8e873f]*/ +{ + (void)module; + return _PyInterpreterState_GET()->jit; +} + +/*[clinic input] +_jit.is_active -> bool +Return True if the topmost Python frame is currently executing JIT code (implies sys._jit.is_enabled()), and False otherwise. +[clinic start generated code]*/ + +static int +_jit_is_active_impl(PyObject *module) +/*[clinic end generated code: output=7facca06b10064d4 input=be2fcd8a269d9b72]*/ +{ + (void)module; + return _PyThreadState_GET()->current_executor != NULL; +} + +static PyMethodDef _jit_methods[] = { + _JIT_IS_AVAILABLE_METHODDEF + _JIT_IS_ENABLED_METHODDEF + _JIT_IS_ACTIVE_METHODDEF + {NULL} +}; + +static struct PyModuleDef _jit_module = { + PyModuleDef_HEAD_INIT, + .m_name = "sys._jit", + .m_doc = _jit_doc, + .m_size = -1, + .m_methods = _jit_methods, +}; + /* Create sys module without all attributes. _PySys_UpdateConfig() should be called later to add remaining attributes. */ PyStatus @@ -4047,6 +4112,16 @@ _PySys_Create(PyThreadState *tstate, PyObject **sysmod_p) goto error; } + PyObject *_jit = _PyModule_CreateInitialized(&_jit_module, PYTHON_API_VERSION); + if (_jit == NULL) { + goto error; + } + err = PyDict_SetItemString(sysdict, "_jit", _jit); + Py_DECREF(_jit); + if (err) { + goto error; + } + assert(!_PyErr_Occurred(tstate)); *sysmod_p = sysmod;