https://github.com/python/cpython/commit/b1aa515bd6b645202eda4ca07e85d92e19b1534d
commit: b1aa515bd6b645202eda4ca07e85d92e19b1534d
branch: main
author: Brandt Bucher <brandtbuc...@microsoft.com>
committer: brandtbucher <brandtbuc...@gmail.com>
date: 2025-05-05T15:25:22-07:00
summary:

GH-133231: Add JIT utilities in sys._jit (GH-133233)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-04-30-19-07-11.gh-issue-133231.H9T8g_.rst
M Doc/library/sys.rst
M Doc/using/cmdline.rst
M Lib/test/libregrtest/utils.py
M Lib/test/support/__init__.py
M Lib/test/test_capi/test_misc.py
M Lib/test/test_dis.py
M Lib/test/test_sys.py
M Modules/_testinternalcapi.c
M Python/clinic/sysmodule.c.h
M Python/sysmodule.c

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 "<stdin>", line 5, in <module>
+                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 <module>\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;

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to