From 03eea7985cf85cea6b89c1faceb62a94e9076e0c Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Fri, 6 Feb 2026 17:48:37 +0000 Subject: [PATCH 1/9] Move _LOAD_ATTR body into external function --- Include/internal/pycore_ceval.h | 5 +++++ Modules/_testinternalcapi/test_cases.c.h | 23 +++---------------- Python/bytecodes.c | 28 +++--------------------- Python/ceval.c | 28 ++++++++++++++++++++++++ Python/executor_cases.c.h | 26 +++++----------------- Python/generated_cases.c.h | 23 +++---------------- 6 files changed, 48 insertions(+), 85 deletions(-) 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/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 { From 062c6e0adb7844d6d9c7d6169281ab0c54a973d8 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Fri, 6 Feb 2026 17:53:11 +0000 Subject: [PATCH 2/9] Add CI --- .github/workflows/tail-call.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index 853d149d20640c..3d2005ba15daa5 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -87,6 +87,14 @@ jobs: ./PCbuild/build.bat --tail-call-interp -c Release -p ${{ matrix.architecture }} ./PCbuild/rt.bat -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3 + - name: Native Windows MSVC with free-threading (release) + if: runner.os == 'Windows' && matrix.architecture != 'ARM64' + shell: pwsh + run: | + $env:PlatformToolset = "v145" + ./PCbuild/build.bat --tail-call-interp --disable-gil -c Release -p ${{ matrix.architecture }} + ./PCbuild/rt.bat -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3 + # No tests (yet): - name: Emulated Windows Clang (release) if: runner.os == 'Windows' && matrix.architecture == 'ARM64' From 0ed50e9020d6c9cd2bb95bd6d46edc4701adeddf Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:59:57 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-06-17-59-47.gh-issue-144549.5BhPlY.rst 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. From fb5ff5938400b073d624f88ac22fba359fff8f8d Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Fri, 6 Feb 2026 18:02:21 +0000 Subject: [PATCH 4/9] add FT for MSVC --- .github/workflows/tail-call.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index 3d2005ba15daa5..bdf1a3f2b61686 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 @@ -88,7 +89,7 @@ jobs: ./PCbuild/rt.bat -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3 - name: Native Windows MSVC with free-threading (release) - if: runner.os == 'Windows' && matrix.architecture != 'ARM64' + if: matrix.target == 'free-threading' shell: pwsh run: | $env:PlatformToolset = "v145" From b64e2e5ff1323ebabd19f660485fafcf90b8d2a6 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Fri, 6 Feb 2026 18:07:49 +0000 Subject: [PATCH 5/9] fix typo --- .github/workflows/tail-call.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index bdf1a3f2b61686..b596fcbffcddda 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -89,7 +89,7 @@ jobs: ./PCbuild/rt.bat -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3 - name: Native Windows MSVC with free-threading (release) - if: matrix.target == 'free-threading' + if: matrix.target == 'free-threading-msvc' shell: pwsh run: | $env:PlatformToolset = "v145" From c3f82d1b84af75938f1b0dae9ef1f75bd1d6e1b9 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Fri, 6 Feb 2026 18:39:33 +0000 Subject: [PATCH 6/9] fix target --- .github/workflows/tail-call.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index b596fcbffcddda..39ab96f1bf8f7a 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -54,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 From cbd3cc802436db588cd2f008827f477739775c97 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Fri, 6 Feb 2026 18:41:22 +0000 Subject: [PATCH 7/9] lint --- .github/workflows/tail-call.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index 39ab96f1bf8f7a..76653c085871b7 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -56,7 +56,7 @@ jobs: runner: windows-2025-vs2026 - target: free-threading-msvc architecture: x64 - runner: windows-2025-vs2026 + runner: windows-2025-vs2026 # - target: aarch64-pc-windows-msvc/msvc # architecture: ARM64 # runner: windows-2022 From f37858e735b45614b848e2736d1f63367428338a Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Fri, 6 Feb 2026 18:46:23 +0000 Subject: [PATCH 8/9] remove double test --- .github/workflows/tail-call.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index 76653c085871b7..01458852bbb6b4 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -84,7 +84,7 @@ 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" From ce063539221fb9e5ab43eae9771cd51913951406 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Fri, 6 Feb 2026 18:52:52 +0000 Subject: [PATCH 9/9] remove tests --- .github/workflows/tail-call.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index 01458852bbb6b4..a47e532d396bc0 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -91,13 +91,13 @@ jobs: ./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 }} - ./PCbuild/rt.bat -p ${{ matrix.architecture }} -q --multiprocess 0 --timeout 4500 --verbose2 --verbose3 # No tests (yet): - name: Emulated Windows Clang (release)