# 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
- 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
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'
_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.
--- /dev/null
+Fix building the tail calling interpreter on Visual Studio 2026 with free-threading.
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 {
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. */
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(
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 {
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 {