]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-144549: Fix tail calling interpreter on Windows for FT (GH-144550)
authorKen Jin <kenjin@python.org>
Fri, 6 Feb 2026 19:20:28 +0000 (03:20 +0800)
committerGitHub <noreply@github.com>
Fri, 6 Feb 2026 19:20:28 +0000 (19:20 +0000)
.github/workflows/tail-call.yml
Include/internal/pycore_ceval.h
Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst [new file with mode: 0644]
Modules/_testinternalcapi/test_cases.c.h
Python/bytecodes.c
Python/ceval.c
Python/executor_cases.c.h
Python/generated_cases.c.h

index 853d149d20640c0e4d5169bef65207a8bc7bf48f..a47e532d396bc0963ff194396bb2e03804290dde 100644 (file)
@@ -38,6 +38,7 @@ jobs:
 # Un-comment as we add support for more platforms for tail-calling interpreters.
 #          - i686-pc-windows-msvc/msvc
           - x86_64-pc-windows-msvc/msvc
+          - free-threading-msvc
 #          - aarch64-pc-windows-msvc/msvc
           - x86_64-apple-darwin/clang
           - aarch64-apple-darwin/clang
@@ -53,6 +54,9 @@ jobs:
           - target: x86_64-pc-windows-msvc/msvc
             architecture: x64
             runner: windows-2025-vs2026
+          - target: free-threading-msvc
+            architecture: x64
+            runner: windows-2025-vs2026
 #          - target: aarch64-pc-windows-msvc/msvc
 #            architecture: ARM64
 #            runner: windows-2022
@@ -80,13 +84,21 @@ jobs:
           python-version: '3.11'
 
       - name: Native Windows MSVC (release)
-        if: runner.os == 'Windows' && matrix.architecture != 'ARM64'
+        if: runner.os == 'Windows' && matrix.architecture != 'ARM64' && matrix.target != 'free-threading-msvc'
         shell: pwsh
         run: |
           $env:PlatformToolset = "v145"
           ./PCbuild/build.bat --tail-call-interp -c Release -p ${{ matrix.architecture }}
           ./PCbuild/rt.bat -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3
 
+        # No tests:
+      - name: Native Windows MSVC with free-threading (release)
+        if: matrix.target == 'free-threading-msvc'
+        shell: pwsh
+        run: |
+          $env:PlatformToolset = "v145"
+          ./PCbuild/build.bat --tail-call-interp --disable-gil -c Release -p ${{ matrix.architecture }}
+
       # No tests (yet):
       - name: Emulated Windows Clang (release)
         if: runner.os == 'Windows' && matrix.architecture == 'ARM64'
index f6bdba3e9916c08a645d399fac0ebadcb59b176d..e9f1f65e53cec12ce8fd78a0befce8606cb27440 100644 (file)
@@ -474,6 +474,11 @@ _Py_assert_within_stack_bounds(
     _PyInterpreterFrame *frame, _PyStackRef *stack_pointer,
     const char *filename, int lineno);
 
+PyAPI_FUNC(_PyStackRef)
+_Py_LoadAttr_StackRefSteal(
+    PyThreadState *tstate, _PyStackRef owner,
+    PyObject *name, _PyStackRef *self_or_null);
+
 // Like PyMapping_GetOptionalItem, but returns the PyObject* instead of taking
 // it as an out parameter. This helps MSVC's escape analysis when used with
 // tail calling.
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst
new file mode 100644 (file)
index 0000000..9679e2c
--- /dev/null
@@ -0,0 +1 @@
+Fix building the tail calling interpreter on Visual Studio 2026 with free-threading.
index 2a73a554eda2cc615be29dd8aeef94a1199f1e59..dda3bc53dc5e0dfc5e1c0e02861765f6052e2af9 100644 (file)
                 self_or_null = &stack_pointer[0];
                 PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1);
                 if (oparg & 1) {
-                    _PyCStackRef method;
                     _PyFrame_SetStackPointer(frame, stack_pointer);
-                    _PyThreadState_PushCStackRef(tstate, &method);
-                    int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref);
+                    attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null);
                     stack_pointer = _PyFrame_GetStackPointer(frame);
-                    if (is_meth) {
-                        assert(!PyStackRef_IsNull(method.ref));
-                        self_or_null[0] = owner;
-                        attr = _PyThreadState_PopCStackRefSteal(tstate, &method);
-                    }
-                    else {
-                        stack_pointer += -1;
-                        ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
-                        _PyFrame_SetStackPointer(frame, stack_pointer);
-                        PyStackRef_CLOSE(owner);
-                        stack_pointer = _PyFrame_GetStackPointer(frame);
-                        self_or_null[0] = PyStackRef_NULL;
-                        attr = _PyThreadState_PopCStackRefSteal(tstate, &method);
-                        if (PyStackRef_IsNull(attr)) {
-                            JUMP_TO_LABEL(error);
-                        }
-                        stack_pointer += 1;
+                    if (PyStackRef_IsNull(attr)) {
+                        JUMP_TO_LABEL(pop_1_error);
                     }
                 }
                 else {
index 818b4fbc3801c03babea9146c016ccd91dff25d1..bd22599aef725dfe473758a858578911f4043881 100644 (file)
@@ -2364,31 +2364,9 @@ dummy_func(
             PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1);
             if (oparg & 1) {
                 /* Designed to work in tandem with CALL, pushes two values. */
-                _PyCStackRef method;
-                _PyThreadState_PushCStackRef(tstate, &method);
-                int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref);
-                if (is_meth) {
-                    /* We can bypass temporary bound method object.
-                       meth is unbound method and obj is self.
-                       meth | self | arg1 | ... | argN
-                     */
-                    assert(!PyStackRef_IsNull(method.ref)); // No errors on this branch
-                    self_or_null[0] = owner;  // Transfer ownership
-                    DEAD(owner);
-                    attr = _PyThreadState_PopCStackRefSteal(tstate, &method);
-                }
-                else {
-                    /* meth is not an unbound method (but a regular attr, or
-                       something was returned by a descriptor protocol).  Set
-                       the second element of the stack to NULL, to signal
-                       CALL that it's not a method call.
-                       meth | NULL | arg1 | ... | argN
-                    */
-                    PyStackRef_CLOSE(owner);
-                    self_or_null[0] = PyStackRef_NULL;
-                    attr = _PyThreadState_PopCStackRefSteal(tstate, &method);
-                    ERROR_IF(PyStackRef_IsNull(attr));
-                }
+                attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null);
+                DEAD(owner);
+                ERROR_IF(PyStackRef_IsNull(attr));
             }
             else {
                 /* Classic, pushes one value. */
index 590b315ab65c2c034de25b61cf4b124e1684f8f3..61644d35b5e473a7aef477247605f69efa51a618 100644 (file)
@@ -1007,6 +1007,34 @@ cleanup:
     return res;
 }
 
+_PyStackRef
+_Py_LoadAttr_StackRefSteal(
+    PyThreadState *tstate, _PyStackRef owner,
+    PyObject *name, _PyStackRef *self_or_null)
+{
+    _PyCStackRef method;
+    _PyThreadState_PushCStackRef(tstate, &method);
+    int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref);
+    if (is_meth) {
+        /* We can bypass temporary bound method object.
+           meth is unbound method and obj is self.
+           meth | self | arg1 | ... | argN
+         */
+        assert(!PyStackRef_IsNull(method.ref)); // No errors on this branch
+        self_or_null[0] = owner;  // Transfer ownership
+        return _PyThreadState_PopCStackRefSteal(tstate, &method);
+    }
+    /* meth is not an unbound method (but a regular attr, or
+       something was returned by a descriptor protocol).  Set
+       the second element of the stack to NULL, to signal
+       CALL that it's not a method call.
+       meth | NULL | arg1 | ... | argN
+    */
+    PyStackRef_CLOSE(owner);
+    self_or_null[0] = PyStackRef_NULL;
+    return _PyThreadState_PopCStackRefSteal(tstate, &method);
+}
+
 #ifdef Py_DEBUG
 void
 _Py_assert_within_stack_bounds(
index a98ec2200485d2e0dcdcc83819d5268be1a0ad3e..f8de66cbce3a9f76872d6afa5a82f58c5f6d4c8f 100644 (file)
             self_or_null = &stack_pointer[1];
             PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1);
             if (oparg & 1) {
-                _PyCStackRef method;
                 stack_pointer[0] = owner;
                 stack_pointer += 1;
                 ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
                 _PyFrame_SetStackPointer(frame, stack_pointer);
-                _PyThreadState_PushCStackRef(tstate, &method);
-                int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref);
+                attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null);
                 stack_pointer = _PyFrame_GetStackPointer(frame);
-                if (is_meth) {
-                    assert(!PyStackRef_IsNull(method.ref));
-                    self_or_null[0] = owner;
-                    attr = _PyThreadState_PopCStackRefSteal(tstate, &method);
-                }
-                else {
-                    stack_pointer += -1;
+                if (PyStackRef_IsNull(attr)) {
+                    stack_pointer[-1] = attr;
+                    stack_pointer += (oparg&1);
                     ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
-                    _PyFrame_SetStackPointer(frame, stack_pointer);
-                    PyStackRef_CLOSE(owner);
-                    stack_pointer = _PyFrame_GetStackPointer(frame);
-                    self_or_null[0] = PyStackRef_NULL;
-                    attr = _PyThreadState_PopCStackRefSteal(tstate, &method);
-                    if (PyStackRef_IsNull(attr)) {
-                        SET_CURRENT_CACHED_VALUES(0);
-                        JUMP_TO_ERROR();
-                    }
-                    stack_pointer += 1;
+                    SET_CURRENT_CACHED_VALUES(0);
+                    JUMP_TO_ERROR();
                 }
             }
             else {
index fc1144a88d70cc5cd5a6a2970e5dbd9abf18e8b8..4cc9d9e03a545dd7928aa8f96ed523c81aed4573 100644 (file)
                 self_or_null = &stack_pointer[0];
                 PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 1);
                 if (oparg & 1) {
-                    _PyCStackRef method;
                     _PyFrame_SetStackPointer(frame, stack_pointer);
-                    _PyThreadState_PushCStackRef(tstate, &method);
-                    int is_meth = _PyObject_GetMethodStackRef(tstate, PyStackRef_AsPyObjectBorrow(owner), name, &method.ref);
+                    attr = _Py_LoadAttr_StackRefSteal(tstate, owner, name, self_or_null);
                     stack_pointer = _PyFrame_GetStackPointer(frame);
-                    if (is_meth) {
-                        assert(!PyStackRef_IsNull(method.ref));
-                        self_or_null[0] = owner;
-                        attr = _PyThreadState_PopCStackRefSteal(tstate, &method);
-                    }
-                    else {
-                        stack_pointer += -1;
-                        ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
-                        _PyFrame_SetStackPointer(frame, stack_pointer);
-                        PyStackRef_CLOSE(owner);
-                        stack_pointer = _PyFrame_GetStackPointer(frame);
-                        self_or_null[0] = PyStackRef_NULL;
-                        attr = _PyThreadState_PopCStackRefSteal(tstate, &method);
-                        if (PyStackRef_IsNull(attr)) {
-                            JUMP_TO_LABEL(error);
-                        }
-                        stack_pointer += 1;
+                    if (PyStackRef_IsNull(attr)) {
+                        JUMP_TO_LABEL(pop_1_error);
                     }
                 }
                 else {