From f89fafed25e2a942cf5713dc7e806f223f859c28 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 27 Mar 2026 20:38:28 +0300 Subject: [PATCH 1/7] Mention _Float16 (type from Annex H of the C23) in the struct docs (#146243) --- Doc/conf.py | 1 + Doc/library/struct.rst | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Doc/conf.py b/Doc/conf.py index 4ac6f6192a0806..07e0d113a24c10 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -177,6 +177,7 @@ ('c:type', '__int64'), ('c:type', 'unsigned __int64'), ('c:type', 'double'), + ('c:type', '_Float16'), # Standard C structures ('c:struct', 'in6_addr'), ('c:struct', 'in_addr'), diff --git a/Doc/library/struct.rst b/Doc/library/struct.rst index 644598d69d6ec4..fa0fb19d852f86 100644 --- a/Doc/library/struct.rst +++ b/Doc/library/struct.rst @@ -254,7 +254,7 @@ platform-dependent. +--------+--------------------------+--------------------+----------------+------------+ | ``N`` | :c:type:`size_t` | integer | | \(3) | +--------+--------------------------+--------------------+----------------+------------+ -| ``e`` | \(6) | float | 2 | \(4) | +| ``e`` | :c:expr:`_Float16` | float | 2 | \(4), \(6) | +--------+--------------------------+--------------------+----------------+------------+ | ``f`` | :c:expr:`float` | float | 4 | \(4) | +--------+--------------------------+--------------------+----------------+------------+ @@ -328,7 +328,9 @@ Notes: revision of the `IEEE 754 standard `_. It has a sign bit, a 5-bit exponent and 11-bit precision (with 10 bits explicitly stored), and can represent numbers between approximately ``6.1e-05`` and ``6.5e+04`` - at full precision. This type is not widely supported by C compilers: on a + at full precision. This type is not widely supported by C compilers: + it's available as :c:expr:`_Float16` type, if the compiler supports the Annex H + of the C23 standard. On a typical machine, an unsigned short can be used for storage, but not for math operations. See the Wikipedia page on the `half-precision floating-point format `_ for more information. From 6763d26b2ba583292140ecca274585c22e61ba33 Mon Sep 17 00:00:00 2001 From: Diego Russo Date: Fri, 27 Mar 2026 17:51:51 +0000 Subject: [PATCH 2/7] GH-126910: reserve FP on AArch64 when generating JIT stencils (GH-146520) --- .../2026-03-27-17-14-18.gh-issue-126910.hooVFQ.rst | 1 + Tools/jit/_targets.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-03-27-17-14-18.gh-issue-126910.hooVFQ.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-27-17-14-18.gh-issue-126910.hooVFQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-27-17-14-18.gh-issue-126910.hooVFQ.rst new file mode 100644 index 00000000000000..e3ddf39408804b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-27-17-14-18.gh-issue-126910.hooVFQ.rst @@ -0,0 +1 @@ +Set frame pointers in ``aarch64-unknown-linux-gnu`` JIT code, allowing most native profilers and debuggers to unwind through them. Patch by Diego Russo diff --git a/Tools/jit/_targets.py b/Tools/jit/_targets.py index fa98dcb5a40851..ad2d5b3c780d54 100644 --- a/Tools/jit/_targets.py +++ b/Tools/jit/_targets.py @@ -593,7 +593,9 @@ def get_target(host: str) -> _COFF32 | _COFF64 | _ELF | _MachO: # -mno-outline-atomics: Keep intrinsics from being emitted. args = ["-fpic", "-mno-outline-atomics", "-fno-plt"] optimizer = _optimizers.OptimizerAArch64 - target = _ELF(host, condition, args=args, optimizer=optimizer) + target = _ELF( + host, condition, args=args, optimizer=optimizer, frame_pointers=True + ) elif re.fullmatch(r"i686-pc-windows-msvc", host): host = "i686-pc-windows-msvc" condition = "defined(_M_IX86)" From b60b9261b5de1e77755cbbeaa7c431e6c475de6e Mon Sep 17 00:00:00 2001 From: Diego Russo Date: Fri, 27 Mar 2026 17:52:48 +0000 Subject: [PATCH 3/7] GH-126910: avoid reading the FP for getting the SP (GH-146521) --- Include/internal/pycore_pystate.h | 13 +------------ Python/pystate.c | 13 +++++++++++++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index 189a8dde9f09ed..a66543cf1eb164 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -312,18 +312,7 @@ static uintptr_t return_pointer_as_int(char* p) { } #endif -static inline uintptr_t -_Py_get_machine_stack_pointer(void) { -#if _Py__has_builtin(__builtin_frame_address) || defined(__GNUC__) - return (uintptr_t)__builtin_frame_address(0); -#elif defined(_MSC_VER) - return (uintptr_t)_AddressOfReturnAddress(); -#else - char here; - /* Avoid compiler warning about returning stack address */ - return return_pointer_as_int(&here); -#endif -} +PyAPI_DATA(uintptr_t) _Py_get_machine_stack_pointer(void); static inline intptr_t _Py_RecursionLimit_GetMargin(PyThreadState *tstate) diff --git a/Python/pystate.c b/Python/pystate.c index 143175da0f45c7..f974c82c391f6a 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3286,3 +3286,16 @@ _Py_GetMainConfig(void) } return _PyInterpreterState_GetConfig(interp); } + +uintptr_t +_Py_get_machine_stack_pointer(void) { +#if _Py__has_builtin(__builtin_frame_address) || defined(__GNUC__) + return (uintptr_t)__builtin_frame_address(0); +#elif defined(_MSC_VER) + return (uintptr_t)_AddressOfReturnAddress(); +#else + char here; + /* Avoid compiler warning about returning stack address */ + return return_pointer_as_int(&here); +#endif +} From 1384f025f5e8ad943c1ac699fd60877b046c0183 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Sat, 28 Mar 2026 03:38:54 +0800 Subject: [PATCH 4/7] gh-126910: Verify that JIT stencils preserve frame pointer (GH-146524) --- Include/internal/pycore_ceval.h | 11 +---------- Include/internal/pycore_pystate.h | 13 ++++++++++++- Python/ceval.c | 13 +++++++++++++ Python/pystate.c | 13 ------------- Tools/jit/_optimizers.py | 17 ++++++++++++++++- Tools/jit/_targets.py | 17 ++++++++++------- 6 files changed, 52 insertions(+), 32 deletions(-) diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index 16913289a02f59..2c83101b6b26fe 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -249,16 +249,7 @@ static inline void _Py_LeaveRecursiveCallTstate(PyThreadState *tstate) { PyAPI_FUNC(void) _Py_InitializeRecursionLimits(PyThreadState *tstate); -static inline int _Py_ReachedRecursionLimit(PyThreadState *tstate) { - uintptr_t here_addr = _Py_get_machine_stack_pointer(); - _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate; - assert(_tstate->c_stack_hard_limit != 0); -#if _Py_STACK_GROWS_DOWN - return here_addr <= _tstate->c_stack_soft_limit; -#else - return here_addr >= _tstate->c_stack_soft_limit; -#endif -} +PyAPI_FUNC(int) _Py_ReachedRecursionLimit(PyThreadState *tstate); // Export for test_peg_generator PyAPI_FUNC(int) _Py_ReachedRecursionLimitWithMargin( diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index a66543cf1eb164..189a8dde9f09ed 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -312,7 +312,18 @@ static uintptr_t return_pointer_as_int(char* p) { } #endif -PyAPI_DATA(uintptr_t) _Py_get_machine_stack_pointer(void); +static inline uintptr_t +_Py_get_machine_stack_pointer(void) { +#if _Py__has_builtin(__builtin_frame_address) || defined(__GNUC__) + return (uintptr_t)__builtin_frame_address(0); +#elif defined(_MSC_VER) + return (uintptr_t)_AddressOfReturnAddress(); +#else + char here; + /* Avoid compiler warning about returning stack address */ + return return_pointer_as_int(&here); +#endif +} static inline intptr_t _Py_RecursionLimit_GetMargin(PyThreadState *tstate) diff --git a/Python/ceval.c b/Python/ceval.c index b4c57b65d13d18..f95900ae01a6af 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1201,6 +1201,19 @@ _PyEval_GetIter(_PyStackRef iterable, _PyStackRef *index_or_null, int yield_from return PyStackRef_FromPyObjectSteal(iter_o); } +Py_NO_INLINE int +_Py_ReachedRecursionLimit(PyThreadState *tstate) { + uintptr_t here_addr = _Py_get_machine_stack_pointer(); + _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate; + assert(_tstate->c_stack_hard_limit != 0); +#if _Py_STACK_GROWS_DOWN + return here_addr <= _tstate->c_stack_soft_limit; +#else + return here_addr >= _tstate->c_stack_soft_limit; +#endif +} + + #if (defined(__GNUC__) && __GNUC__ >= 10 && !defined(__clang__)) && defined(__x86_64__) /* * gh-129987: The SLP autovectorizer can cause poor code generation for diff --git a/Python/pystate.c b/Python/pystate.c index f974c82c391f6a..143175da0f45c7 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -3286,16 +3286,3 @@ _Py_GetMainConfig(void) } return _PyInterpreterState_GetConfig(interp); } - -uintptr_t -_Py_get_machine_stack_pointer(void) { -#if _Py__has_builtin(__builtin_frame_address) || defined(__GNUC__) - return (uintptr_t)__builtin_frame_address(0); -#elif defined(_MSC_VER) - return (uintptr_t)_AddressOfReturnAddress(); -#else - char here; - /* Avoid compiler warning about returning stack address */ - return return_pointer_as_int(&here); -#endif -} diff --git a/Tools/jit/_optimizers.py b/Tools/jit/_optimizers.py index 83c878d8fe205b..ef28e0c0ddeac8 100644 --- a/Tools/jit/_optimizers.py +++ b/Tools/jit/_optimizers.py @@ -162,6 +162,7 @@ class Optimizer: label_prefix: str symbol_prefix: str re_global: re.Pattern[str] + frame_pointers: bool # The first block in the linked list: _root: _Block = dataclasses.field(init=False, default_factory=_Block) _labels: dict[str, _Block] = dataclasses.field(init=False, default_factory=dict) @@ -193,6 +194,7 @@ class Optimizer: _re_small_const_1 = _RE_NEVER_MATCH _re_small_const_2 = _RE_NEVER_MATCH const_reloc = "" + _frame_pointer_modify: typing.ClassVar[re.Pattern[str]] = _RE_NEVER_MATCH def __post_init__(self) -> None: # Split the code into a linked list of basic blocks. A basic block is an @@ -553,6 +555,16 @@ def _small_const_2(self, inst: Instruction) -> tuple[str, Instruction | None]: def _small_consts_match(self, inst1: Instruction, inst2: Instruction) -> bool: raise NotImplementedError() + def _validate(self) -> None: + for block in self._blocks(): + if not block.instructions: + continue + for inst in block.instructions: + if self.frame_pointers: + assert ( + self._frame_pointer_modify.match(inst.text) is None + ), "Frame pointer should not be modified" + def run(self) -> None: """Run this optimizer.""" self._insert_continue_label() @@ -565,6 +577,7 @@ def run(self) -> None: self._remove_unreachable() self._fixup_external_labels() self._fixup_constants() + self._validate() self.path.write_text(self._body()) @@ -595,6 +608,7 @@ class OptimizerAArch64(Optimizer): # pylint: disable = too-few-public-methods r"\s*(?Pldr)\s+.*(?P_JIT_OP(ARG|ERAND(0|1))_(16|32)).*" ) const_reloc = "CUSTOM_AARCH64_CONST" + _frame_pointer_modify = re.compile(r"\s*stp\s+x29.*") def _get_reg(self, inst: Instruction) -> str: _, rest = inst.text.split(inst.name) @@ -649,4 +663,5 @@ class OptimizerX86(Optimizer): # pylint: disable = too-few-public-methods # https://www.felixcloutier.com/x86/jmp _re_jump = re.compile(r"\s*jmp\s+(?P[\w.]+)") # https://www.felixcloutier.com/x86/ret - _re_return = re.compile(r"\s*ret\b") + _re_return = re.compile(r"\s*retq?\b") + _frame_pointer_modify = re.compile(r"\s*movq?\s+%(\w+),\s+%rbp.*") diff --git a/Tools/jit/_targets.py b/Tools/jit/_targets.py index ad2d5b3c780d54..787fcf53260f3d 100644 --- a/Tools/jit/_targets.py +++ b/Tools/jit/_targets.py @@ -176,8 +176,9 @@ async def _compile( f"{s}", f"{c}", ] + is_shim = opname == "shim" if self.frame_pointers: - frame_pointer = "all" if opname == "shim" else "reserved" + frame_pointer = "all" if is_shim else "reserved" args_s += ["-Xclang", f"-mframe-pointer={frame_pointer}"] args_s += self.args # Allow user-provided CFLAGS to override any defaults @@ -185,12 +186,14 @@ async def _compile( await _llvm.run( "clang", args_s, echo=self.verbose, llvm_version=self.llvm_version ) - self.optimizer( - s, - label_prefix=self.label_prefix, - symbol_prefix=self.symbol_prefix, - re_global=self.re_global, - ).run() + if not is_shim: + self.optimizer( + s, + label_prefix=self.label_prefix, + symbol_prefix=self.symbol_prefix, + re_global=self.re_global, + frame_pointers=self.frame_pointers, + ).run() args_o = [f"--target={self.triple}", "-c", "-o", f"{o}", f"{s}"] await _llvm.run( "clang", args_o, echo=self.verbose, llvm_version=self.llvm_version From 73cc1fd4f45b4daf2b2f9a6be69148775c7c2bff Mon Sep 17 00:00:00 2001 From: Imgyu Kim Date: Sat, 28 Mar 2026 05:48:07 +0900 Subject: [PATCH 5/7] gh-146310: Fix ensurepip to treat empty WHEEL_PKG_DIR as unset (#146357) Path('') resolves to CWD, so an empty WHEEL_PKG_DIR string caused ensurepip to search the current working directory for wheel files. Add a truthiness check to treat empty strings the same as None. Co-authored-by: Victor Stinner --- Lib/ensurepip/__init__.py | 3 ++- Lib/test/test_ensurepip.py | 10 ++++++++++ .../2026-03-24-03-49-50.gh-issue-146310.WhlDir.rst | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-24-03-49-50.gh-issue-146310.WhlDir.rst diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index 6164ea62324cce..93b4e7a820f3ad 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -16,7 +16,8 @@ # policies recommend against bundling dependencies. For example, Fedora # installs wheel packages in the /usr/share/python-wheels/ directory and don't # install the ensurepip._bundled package. -if (_pkg_dir := sysconfig.get_config_var('WHEEL_PKG_DIR')) is not None: +_pkg_dir = sysconfig.get_config_var('WHEEL_PKG_DIR') +if _pkg_dir: _WHEEL_PKG_DIR = Path(_pkg_dir).resolve() else: _WHEEL_PKG_DIR = None diff --git a/Lib/test/test_ensurepip.py b/Lib/test/test_ensurepip.py index c62b340f6a340f..20a56ed715d8ab 100644 --- a/Lib/test/test_ensurepip.py +++ b/Lib/test/test_ensurepip.py @@ -7,6 +7,7 @@ import unittest import unittest.mock from pathlib import Path +from test.support import import_helper import ensurepip import ensurepip._uninstall @@ -36,6 +37,15 @@ def test_version_no_dir(self): # when the bundled pip wheel is used, we get _PIP_VERSION self.assertEqual(ensurepip._PIP_VERSION, ensurepip.version()) + def test_wheel_pkg_dir_none(self): + # gh-146310: empty or None WHEEL_PKG_DIR should not search CWD + for value in ('', None): + with unittest.mock.patch('sysconfig.get_config_var', + return_value=value) as get_config_var: + module = import_helper.import_fresh_module('ensurepip') + self.assertIsNone(module._WHEEL_PKG_DIR) + get_config_var.assert_called_once_with('WHEEL_PKG_DIR') + def test_selected_wheel_path_no_dir(self): pip_filename = f'pip-{ensurepip._PIP_VERSION}-py3-none-any.whl' with unittest.mock.patch.object(ensurepip, '_WHEEL_PKG_DIR', None): diff --git a/Misc/NEWS.d/next/Library/2026-03-24-03-49-50.gh-issue-146310.WhlDir.rst b/Misc/NEWS.d/next/Library/2026-03-24-03-49-50.gh-issue-146310.WhlDir.rst new file mode 100644 index 00000000000000..b712595585201b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-24-03-49-50.gh-issue-146310.WhlDir.rst @@ -0,0 +1,2 @@ +The :mod:`ensurepip` module no longer looks for ``pip-*.whl`` wheel packages +in the current directory. From 69b08c397b92b6353406cec53f2b927fab1199d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Fri, 27 Mar 2026 22:35:50 +0100 Subject: [PATCH 6/7] gh-145057: Fix test names and comments to reflect `sys.lazy_modules` is a dict, not a set (#146084) Fix test names and comments to reflect sys.lazy_modules is a dict, not a set --- Lib/test/test_lazy_import/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 328f2906f90159..6e53e2c042bc16 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -484,8 +484,8 @@ def my_filter(name): sys.set_lazy_imports_filter(my_filter) self.assertIs(sys.get_lazy_imports_filter(), my_filter) - def test_lazy_modules_attribute_is_set(self): - """sys.lazy_modules should be a set per PEP 810.""" + def test_lazy_modules_attribute_is_dict(self): + """sys.lazy_modules should be a dict per PEP 810.""" self.assertIsInstance(sys.lazy_modules, dict) @support.requires_subprocess() @@ -966,7 +966,7 @@ def test_module_added_to_lazy_modules_on_lazy_import(self): def test_lazy_modules_is_per_interpreter(self): """Each interpreter should have independent sys.lazy_modules.""" - # Basic test that sys.lazy_modules exists and is a set + # Basic test that sys.lazy_modules exists and is a dict self.assertIsInstance(sys.lazy_modules, dict) @@ -1575,7 +1575,7 @@ def access_modules(idx): self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") self.assertIn("OK", result.stdout) - def test_concurrent_lazy_modules_set_updates(self): + def test_concurrent_lazy_modules_dict_updates(self): """Multiple threads creating lazy imports should safely update sys.lazy_modules.""" code = textwrap.dedent(""" import sys From a5b9d60a69d9ca281f956d5ec48fcaededd1b94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Fri, 27 Mar 2026 22:37:11 +0100 Subject: [PATCH 7/7] gh-145059: Record lazy modules without submodules in `sys.lazy_modules` (#146081) Record simple lazy modules as well --- Lib/test/test_lazy_import/__init__.py | 15 +++++++++++++++ ...2026-03-17-14-20-56.gh-issue-145059.aB3xKm.rst | 1 + Python/import.c | 6 ++++++ 3 files changed, 22 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-03-17-14-20-56.gh-issue-145059.aB3xKm.rst diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 6e53e2c042bc16..69cb96cf4a0c1a 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -969,6 +969,21 @@ def test_lazy_modules_is_per_interpreter(self): # Basic test that sys.lazy_modules exists and is a dict self.assertIsInstance(sys.lazy_modules, dict) + def test_lazy_module_without_children_is_tracked(self): + code = textwrap.dedent(""" + import sys + lazy import json + assert "json" in sys.lazy_modules, ( + f"expected 'json' in sys.lazy_modules, got {set(sys.lazy_modules)}" + ) + assert sys.lazy_modules["json"] == set(), ( + f"expected empty set for sys.lazy_modules['json'], " + f"got {sys.lazy_modules['json']!r}" + ) + print("OK") + """) + assert_python_ok("-c", code) + @support.requires_subprocess() class CommandLineAndEnvVarTests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-17-14-20-56.gh-issue-145059.aB3xKm.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-17-14-20-56.gh-issue-145059.aB3xKm.rst new file mode 100644 index 00000000000000..e2db5a83a1a9e3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-17-14-20-56.gh-issue-145059.aB3xKm.rst @@ -0,0 +1 @@ +Fixed ``sys.lazy_modules`` to include lazy modules without submodules. Patch by Bartosz Sławecki. diff --git a/Python/import.c b/Python/import.c index f615fe37ba8fbe..e298fbee536c1b 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4377,6 +4377,12 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0, PyUnicode_GET_LENGTH(name), -1); if (dot < 0) { + PyObject *lazy_submodules = ensure_lazy_submodules( + (PyDictObject *)lazy_modules, name); + if (lazy_submodules == NULL) { + goto done; + } + Py_DECREF(lazy_submodules); ret = 0; goto done; }