https://github.com/python/cpython/commit/895e83727d6d686ab7dd5494f3ccd52b0726502e
commit: 895e83727d6d686ab7dd5494f3ccd52b0726502e
branch: main
author: Ken Jin <[email protected]>
committer: Fidget-Spinner <[email protected]>
date: 2026-02-06T19:20:28Z
summary:

gh-144549: Fix tail calling interpreter on Windows for FT (GH-144550)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst
M .github/workflows/tail-call.yml
M Include/internal/pycore_ceval.h
M Modules/_testinternalcapi/test_cases.c.h
M Python/bytecodes.c
M Python/ceval.c
M Python/executor_cases.c.h
M Python/generated_cases.c.h

diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml
index 853d149d20640c..a47e532d396bc0 100644
--- a/.github/workflows/tail-call.yml
+++ b/.github/workflows/tail-call.yml
@@ -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'
diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h
index f6bdba3e9916c0..e9f1f65e53cec1 100644
--- a/Include/internal/pycore_ceval.h
+++ b/Include/internal/pycore_ceval.h
@@ -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
index 00000000000000..9679e2cf6af426
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst
@@ -0,0 +1 @@
+Fix building the tail calling interpreter on Visual Studio 2026 with 
free-threading.
diff --git a/Modules/_testinternalcapi/test_cases.c.h 
b/Modules/_testinternalcapi/test_cases.c.h
index 2a73a554eda2cc..dda3bc53dc5e0d 100644
--- a/Modules/_testinternalcapi/test_cases.c.h
+++ b/Modules/_testinternalcapi/test_cases.c.h
@@ -7899,28 +7899,11 @@
                 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 {
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 818b4fbc3801c0..bd22599aef725d 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -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. */
diff --git a/Python/ceval.c b/Python/ceval.c
index 590b315ab65c2c..61644d35b5e473 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -1007,6 +1007,34 @@ _Py_BuildMap_StackRefSteal(
     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(
diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h
index a98ec2200485d2..f8de66cbce3a9f 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -8670,32 +8670,18 @@
             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 {
diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h
index fc1144a88d70cc..4cc9d9e03a545d 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -7898,28 +7898,11 @@
                 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 {

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to