Files
Python3-Win9x/PORT_STATUS.md
T

49 KiB

Port Status — CPython 3.11.16 to Windows 98 SE

Date: 2026-08-17 Baseline: CPython v3.11.16 Compiler: MSVC 6.0 (cl.exe 12.00.8804), from external MSVC600 Host test environment: Linux + Wine

Files changed in this revision (pylifecycle.c VC6 PyRuntime section slice)

All still uncommitted; nothing in this list has been pushed anywhere.

  • cpython/Python/pylifecycle.c — narrow VC6-only compatibility edit for the first Python/pylifecycle.c blocker: keep the modern Windows #pragma section("PyRuntime", read, write) + __declspec(allocate("PyRuntime")) path unchanged for non-VC6 compilers, but use VC6's supported #pragma data_seg("PyRuntime") / #pragma data_seg() spelling around the initialized _PyRuntime object when _MSC_VER < 1300. The _PyRuntimeState_INIT initializer remains exactly at the definition site.
  • compat/msvc600/cpython-3.11.16-vc6-headers.patch — regenerated in full from git -C cpython diff; now 767 lines and includes the Python/pylifecycle.c hunk alongside previous uncommitted compatibility edits.
  • PORT_STATUS.md — this revision.

Command results (all reproduced in this revision):

Command Result
bash scripts/vc6-probe.test.sh exit 0, ALL TESTS PASSED — 6/6 checks ok
bash scripts/vc6-probe-core.test.sh exit 0, ALL TESTS PASSED — 27/27 checks ok; includes Modules/main.c compile-only clean check
bash scripts/vc6-probe-core.sh cpython/Python/pylifecycle.c exit 2 — gets past the prior VC6-incompatible PyRuntime section pragma/declaration pair; no unknown pragma warning and no C2341 segment error remain. New first blocker is cpython\Python\pylifecycle.c(112) : error C2059: syntax error : '.' in _PyRuntimeState_INIT, followed by more C99 compound-literal/designated-initializer and C89 mixed-declaration errors; the run later hits fatal error C1003 at line 1232.

Caveat: all probe results here are compile-only (/c) results. No link, no pythoncore/python.exe build, no Wine launch, and no Windows 98 runtime execution was attempted or claimed.

Verified

  • CPython source checkout is at the requested v3.11.16 baseline.

  • The MSVC600 tool bundle is present locally and its VC98/bin/VCVARS32.BAT environment script runs under Wine.

  • MSVC6 reports:

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 12.00.8804 for 80x86
    
  • A direct compile probe reaches CPython 3.11 headers. The first unmodified probe fails before linking, proving the compiler is being invoked against the intended baseline.

  • Project-local compatibility headers have been added under compat/msvc600/: inttypes.h (first isolated shim) and, as of this revision, stdbool.h (see "stdbool slice" below). Neither is declared sufficient for the port on its own.

  • scripts/vc6-probe.sh was fixed and is now truthful. The script piped cl.exe output through grep/sed to drop Wine's :err: debug noise and to shorten absolute host paths to repo-relative ones. The path-shortening step was silently broken: it built a sed pattern directly from a Windows-style path (e.g. Z:\home\ubuntu\...\), and both sed BRE and bash glob patterns treat a bare \ in a pattern as an escape character, so \h, \u, \p, etc. were consumed as (mostly no-op) escapes instead of literal characters and the substitution never matched anything — diagnostics always showed full absolute Wine paths instead of the intended repo-relative ones. Fixed by matching the prefix as a literal string via a quoted bash ${line//"$prefix"/} expansion (quoting forces literal, not glob, matching) instead of an unescaped regex. The pipeline's exit-status capture via PIPESTATUS[0] was already correct (verified with a deliberately broken translation unit: cl.exe's real nonzero exit code propagated correctly both before and after this fix); it is now covered by a regression test so it cannot regress silently. A new regression test, scripts/vc6-probe.test.sh, exercises both the path-stripping helper directly and — when wine + MSVC600 are available, as they are in this environment — the real script against a deliberately invalid .c file, asserting a nonzero exit code, a surfaced error C diagnostic, and no leaked absolute Wine path. Run: bash scripts/vc6-probe.test.sh. Last run: all 6 checks ok.

  • **Include/internal/pycore_atomic.h's VC6/intrin.h blocker is now resolved for 32-bit x86 (_M_IX86) and verified; see "Atomics slice" below for full detail and honesty notes on what is and is not covered.

  • **Include/internal/pycore_interp.h's VC6/<stdbool.h> blocker is now resolved and verified; see "stdbool slice" below for full detail.

  • **The VC6 C89-vs-C99 mixed-declarations blocker in four Include/internal/pycore_*.h headers (pycore_code.h, pycore_dict.h, pycore_list.h, pycore_call.h) reachable from Modules/main.c is now resolved and verified; see "Mixed-declarations header slice" below for full detail.

  • The same C89-vs-C99 mixed-declarations pattern, pervasive within Modules/main.c's own body (29 error sites across roughly a dozen functions), is now also resolved and verified; see "Modules/main.c declaration-order slice" below for full detail. With that fix applied, Modules/main.c hits a new, unrelated, and out-of-scope-for-that-slice blocker: several C99 compound-literal/designated-initializer macros (_PyStatus_OK(), _PyCompilerFlags_INIT, and the _PyArgv-style struct literals in Py_Main()/Py_BytesMain()) that VC6 does not support at all, independent of declaration position.

  • That compound-literal/designated-initializer blocker is now also resolved and verified for all 8 sites in Modules/main.c; see "Compound-literal / designated-initializer slice" below for full detail. With that fix applied, cpython/Modules/main.c compiles with cl.exe exit status 0 under scripts/vc6-probe-core.sh — no errors, no warnings. This is a compile-only (/c) result for one translation unit; it is not a link, not a pythoncore/python.exe build, and not a runtime claim (vc6-probe-core.sh never attempts to link). Probing the next core translation unit in Modules/main.c's own call chain, Python/pylifecycle.c, hits the same two already-solved blocker classes again (recurring, not novel) plus enough additional errors to exceed the compiler's 100-error cap before the whole file is even seen; see "Next blocker: Python/pylifecycle.c" below. No link or runtime success is claimed anywhere in this document.

pycore_object.h VC6 __func__ / GC helper declaration-order slice

Scope: one internal header only, as requested. The real Python/pylifecycle.c core probe previously stopped first in Include/internal/pycore_object.h: VC6 reported __func__ undeclared at the _PyObject_ASSERT_FROM(..., __func__) calls in _PyObject_GC_TRACK(), then cascaded into C89 mixed-declaration errors for the local PyGC_Head * and PyInterpreterState * declarations in _PyObject_GC_TRACK() and _PyObject_GC_UNTRACK().

The header now has a VC6-only fallback:

#if defined(_MSC_VER) && _MSC_VER < 1300 && !defined(__func__)
#  define __func__ "<unknown>"
#endif

A quick VC6 smoke probe showed __FUNCTION__ is also unavailable in VC6, so there was no exact function-name extension to preserve. This mapping only degrades debug/fatal diagnostic function-name text on VC6; C99 compilers and newer MSVC keep their normal behavior. The only declaration-order edits are the mechanical C89 transform in _PyObject_GC_TRACK() and _PyObject_GC_UNTRACK(): declarations at block start, assignments left at the original evaluation points.

Verification: bash scripts/vc6-probe-core.sh cpython/Python/pylifecycle.c now gets past pycore_object.h; the new first blocker is the Windows section-allocation pragma pair in Python/pylifecycle.c (see "Next blocker" below).

Atomics slice (pycore_atomic.h, VC6 / _M_IX86)

Scope: 32-bit x86 only, matching this port's only target (Windows 9x has no x64 edition). Include/internal/pycore_atomic.h previously included <intrin.h> and used _Interlocked* compiler intrinsics unconditionally for any _MSC_VER-defined compiler; VC6 (_MSC_VER 1200) has neither. The now-applied, still-uncommitted compat/msvc600/cpython-3.11.16-vc6-headers.patch adds two things to this header, both gated strictly to defined(_MSC_VER) && _MSC_VER < 1300 && defined(_M_IX86) (a new _Py_ATOMIC_VC6_X86 macro) so every other compiler/version — including modern MSVC on _M_X64/ARM and the GCC/clang/C11 paths — keeps its exact prior behavior, byte-for-byte (verified: git diff on the non-VC6 branches shows only their line numbers moving under the new #elif, no content changes):

  1. The top-of-file <intrin.h>/<immintrin.h> include is skipped for VC6/_M_IX86; <windows.h> is included instead, to declare InterlockedExchange/InterlockedCompareExchange.
  2. A new #if defined(_Py_ATOMIC_VC6_X86) branch (ahead of the existing _M_IX86 || _M_X64 branch, now #elif) implements _Py_atomic_store_explicit/_Py_atomic_load_explicit for _Py_atomic_int/_Py_atomic_address using the Win32 InterlockedExchange/InterlockedCompareExchange API — present in KERNEL32.DLL since Windows 98 (confirmed against this project's actual MSVC600 VC98 SDK headers, MSVC600/VC98/Include/WINBASE.H, which declare both for _M_IX86 outside the MRx000/Alpha-only intrinsic path). Both give a full memory barrier on x86, same as the intrinsics they replace, so — as the original comment already notes for the HLE-unavailable case — every _Py_memory_order maps to the same fully-fenced operation; VC6 has no HLE (_HLEAcquire/_HLERelease, VS2013+) intrinsics or hardware support to preserve here regardless.

64-bit atomics are explicitly NOT implemented, and this is not a gap in practice for this port. _Py_atomic_address (uintptr_t) and _Py_atomic_int (int) are both 4 bytes on 32-bit x86, so the existing sizeof(...) == 8 branches that select the 64-bit store/load paths in the pre-existing _M_IX86 || _M_X64 code are already unreachable dead code on _M_IX86 upstream, before this patch — VC6 never defines _M_X64. The VC6 branch added here therefore has no 64-bit path to implement, and a #error guards the (unreachable-under-VC6) case where _M_X64 were ever also defined, so a future misconfiguration fails loudly at compile time instead of silently truncating a 64-bit atomic. No CMPXCHG8B inline assembly was written, and none is required for this header; if some other part of the port needs genuine 64-bit interlocked ops on VC6 later, that is a separate, not-yet-encountered problem, not something papered over here.

Verified with two independent probes, both under scripts/vc6-probe-core.sh (new; extends scripts/vc6-probe.sh's approach to Py_BUILD_CORE translation units — /D Py_BUILD_CORE plus Include/internal on the include path):

  • scripts/fixtures/vc6_atomic_smoke.c (new) — a minimal, isolated translation unit that includes only pycore_atomic.h (via <inttypes.h> for uintptr_t, which real callers get from Python.h/pyport.h) and exercises _Py_atomic_store/_Py_atomic_load and their _relaxed variants on both _Py_atomic_int and _Py_atomic_address:

    $ bash scripts/vc6-probe-core.sh scripts/fixtures/vc6_atomic_smoke.c
    == MSVC600 Py_BUILD_CORE compile probe ==
    source : scripts/fixtures/vc6_atomic_smoke.c
    ...
    vc6_atomic_smoke.c
    
    == cl.exe exit status: 0 ==
    

    No errors, no warnings.

  • cpython/Modules/main.c, the same real core translation unit identified as the next blocker previously. It now compiles past pycore_atomic.h (reached transitively via pycore_runtime.h before pycore_interp.h) and hits a new, different, and unrelated blocker:

    $ bash scripts/vc6-probe-core.sh
    == MSVC600 Py_BUILD_CORE compile probe ==
    source : cpython/Modules/main.c
    ...
    cpython\Include\internal\pycore_interp.h(11) : fatal error C1083:
    Cannot open include file: 'stdbool.h': No such file or directory
    
    == cl.exe exit status: 2 ==
    

    Include/internal/pycore_interp.h line 11 does #include <stdbool.h> before its own #include "pycore_atomic.h" on line 13 — a C99 standard header VC6 does not ship, unrelated to atomics. This is the next blocker for this port and is left unaddressed here, out of scope for this slice.

Both probes are covered by a new regression test, scripts/vc6-probe-core.test.sh, which additionally asserts (so this result cannot regress silently) that a deliberately broken translation unit still fails with a surfaced error C diagnostic, and that Modules/main.c's current failure is specifically not at pycore_atomic.h/intrin.h (i.e. that blocker has not resurfaced). Run: bash scripts/vc6-probe-core.test.sh. At the time this atomics slice was written, this was all 6 checks ok; the stdbool slice below added 5 more checks to the same file (now 11; see that section for the current count and last-run result). scripts/vc6-probe.test.sh was re-run after the atomics change and still passed (all 6 checks ok), confirming the public-header probe was unaffected.

stdbool slice (pycore_interp.h, VC6 / C99 <stdbool.h>)

Include/internal/pycore_interp.h line 11 does #include <stdbool.h> before its own #include "pycore_atomic.h" on line 13 — a C99 standard header VC6 (_MSC_VER 1200, which predates C99 entirely) does not ship. In VC6's C mode, bool/true/false are not keywords at all (they are C++-only there), and there is no standard-library <stdbool.h> to fall back on.

The now-added, still-uncommitted compat/msvc600/stdbool.h is a project-local fallback for this single header, not a patch to CPython source. It is picked up purely through include-path ordering: both scripts/vc6-probe.sh and scripts/vc6-probe-core.sh already put compat/msvc600 first on the VC6 /I include path (needed earlier for compat/msvc600/inttypes.h), so VC6's #include <stdbool.h> resolves to this project-local header before it would ever reach a (nonexistent) system one. Nothing under cpython/ changes for this slice, so compat/msvc600/cpython-3.11.16-vc6-headers.patch is unchanged. Every other compiler is unaffected for the same reason a browser's /I search order doesn't affect other browsers: compat/msvc600 is never added to any other compiler's include path, so a conforming C99/C11 compiler still gets its own real <stdbool.h> (or built-in _Bool/bool), untouched. As defence in depth (not because it is expected to be reachable), the header itself also #errors out if included under anything but _MSC_VER < 1300.

The shim defines the same four names as the standard C99 header — bool (as int, since VC6 has no _Bool built-in type to alias), true, false, and __bool_true_false_are_defined — as plain #define macros rather than a typedef, matching the standard's requirement that a conforming program be able to #undef/redefine bool/true/false.

Verified with the same two-probe approach used for the atomics slice:

  • scripts/fixtures/vc6_stdbool_smoke.c (new) — a minimal, isolated translation unit that includes only <stdbool.h> and exercises bool/true/false/__bool_true_false_are_defined:

    $ bash scripts/vc6-probe-core.sh scripts/fixtures/vc6_stdbool_smoke.c
    == MSVC600 Py_BUILD_CORE compile probe ==
    source : scripts/fixtures/vc6_stdbool_smoke.c
    ...
    vc6_stdbool_smoke.c
    
    == cl.exe exit status: 0 ==
    

    No errors, no warnings.

  • cpython/Modules/main.c, the same real core translation unit used to verify the atomics slice. It now compiles past pycore_interp.h line 11 (reached the same way as before, transitively via pycore_runtime.h) — the fatal stdbool.h/C1083 error is gone — and proceeds through pycore_atomic.h and pycore_ast_state.h before hitting a new, unrelated, and considerably larger blocker; see "Current first-blocker evidence" below for the full detail.

Both probes are covered by 5 new checks added to the existing scripts/vc6-probe-core.test.sh (positive isolated-fixture compile, no-errors check, a negative deliberately-broken-fixture check, its error-surfaced check, and a check that Modules/main.c's output no longer contains Cannot open include file: 'stdbool.h'), bringing that file to 11 checks total. Run: bash scripts/vc6-probe-core.test.sh. Last run: all 11 checks ok (wine + MSVC600 available in this environment). scripts/vc6-probe.test.sh was re-run after this change and still passes (all 6 checks ok), confirming the public-header probe is unaffected.

Mixed-declarations header slice (pycore_code.h, pycore_dict.h, pycore_list.h, pycore_call.h)

Scope: the four Include/internal/pycore_*.h headers that the Modules/main.c #include chain directly reaches with VC6 C89 mixed-declaration violations, and no others. This is a bounded slice of the "Compiler language gap" blocker recorded below, not an attempt at the whole gap: it fixes exactly the sites the real core probe (Modules/main.c via scripts/vc6-probe-core.sh) hits inside CPython's own headers, and stops there. Modules/main.c itself has the same pattern pervasively throughout its own function bodies; that is explicitly out of scope for this slice and is now the next recorded blocker (see "Current first-blocker evidence" below) rather than something papered over here.

Six static inline functions across the four headers declared a variable after a preceding statement (typically an assert(...)), which C89 (VC6's only supported dialect, give or take Microsoft extensions) does not allow inside a block — only C99 does. VC6's parser, on hitting the declaration where it expects a statement, fails to recognize it as a declaration at all and instead cascades into unrelated-looking C2065/C2109/C2106 errors for the rest of the block, exactly as described for the first instance (pycore_code.h:154) in earlier revisions of this document.

The now-applied, still-uncommitted compat/msvc600/cpython-3.11.16-vc6-headers.patch fixes each site the same way: hoist the declaration (no initializer) to the top of its enclosing block, and turn the original TYPE name = expr; into a plain assignment (name = expr;) at its original position. This changes no evaluation order and no semantics — it is the standard, mechanical C89 transform for this exact pattern — and is applied only to the six functions that actually needed it, not the surrounding file:

  • Include/internal/pycore_code.h: _PyLocals_GetKind(), _PyLocals_SetKind() (both hoist a char *ptr), and adaptive_counter_backoff() (hoists unsigned int value).
  • Include/internal/pycore_dict.h: _PyDictValues_AddToInsertionOrder() (hoists uint8_t *size_ptr and int size).
  • Include/internal/pycore_list.h: _PyList_AppendTakeRef() (hoists Py_ssize_t len and Py_ssize_t allocated).
  • Include/internal/pycore_call.h: _PyVectorcall_FunctionInline() (hoists PyTypeObject *tp, Py_ssize_t offset, and vectorcallfunc ptr).

Every other compiler is unaffected: these are plain C source edits (no #ifdef/_MSC_VER gating, unlike the atomics slice), and hoisting a declaration without an initializer, then assigning immediately after the same statements that used to compute the initializer, is behavior-identical under C89, C99, C11, and C++ alike — no code path, value, or evaluation order changes for any compiler already accepting the original source.

Verified with the same two-probe approach used for the atomics and stdbool slices:

  • scripts/fixtures/vc6_mixed_decls_smoke.c (new) — includes the real Python.h (these four headers assume public types like PyObject, PyCodeObject, PyListObject, PyDictValues, and vectorcallfunc already declared, the same way real CPython .c sources reach them, so full isolation without Python.h is not meaningful here unlike the atomics/stdbool fixtures) plus all four headers, and calls all six fixed functions:

    $ bash scripts/vc6-probe-core.sh scripts/fixtures/vc6_mixed_decls_smoke.c
    == MSVC600 Py_BUILD_CORE compile probe ==
    source : scripts/fixtures/vc6_mixed_decls_smoke.c
    ...
    vc6_mixed_decls_smoke.c
    ...
    == cl.exe exit status: 0 ==
    

    No errors, no warnings.

  • cpython/Modules/main.c, the same real core translation unit used for the earlier slices. All four headers now compile clean — no pycore_code.h/pycore_dict.h/pycore_list.h/pycore_call.h error appears anywhere in the probe output — and the compiler proceeds entirely into Modules/main.c's own body, where a new, considerably larger, and out-of-scope-for-this-slice blocker is hit; see "Current first-blocker evidence" below.

Both probes are covered by 5 new checks added to scripts/vc6-probe-core.test.sh (positive isolated-fixture compile, no-errors check, a negative deliberately-broken-fixture check, its error-surfaced check, and a check that Modules/main.c's output no longer contains any pycore_code.h/pycore_dict.h/pycore_list.h/ pycore_call.h error line), bringing that file to 16 checks total. Run: bash scripts/vc6-probe-core.test.sh. Last run: all 16 checks ok (wine + MSVC600 available in this environment). scripts/vc6-probe.test.sh was re-run after this change and still passes (all 6 checks ok), confirming the public-header probe is unaffected. The patch's reproducibility was also re-verified directly: git apply --check --directory=cpython compat/msvc600/cpython-3.11.16-vc6-headers.patch against a freshly stashed (pristine v3.11.16) cpython/ working tree exits 0.

Modules/main.c declaration-order slice

Scope: the C89-vs-C99 mixed-declarations pattern inside Modules/main.c's own function bodies only — declaration position, not any other C99 feature. No other file was touched, and no whole-tree mechanical rewrite was attempted. This is the follow-on to the "Mixed-declarations header slice" above: the same pattern (a block-scope declaration appearing after a preceding statement, which C89's parser cannot recognize as a declaration), but recurring roughly a dozen times across Modules/main.c's own ~740 lines rather than in a handful of small header functions.

The now-applied, still-uncommitted compat/msvc600/cpython-3.11.16-vc6-headers.patch fixes every site the same mechanical way used for the header slice: hoist the bare declaration (no initializer) to the top of its enclosing block, and turn the original TYPE name = expr; into a plain assignment (name = expr;) at its original position. This changes no evaluation order and no semantics for any compiler, VC6 or otherwise. Two variants of this same transform were needed, both already implicit in the mechanical rule but worth calling out explicitly:

  • A declaration whose original initializer is a side-effecting call (e.g. FILE *fp = _Py_fopen_obj(filename, "rb"); in pymain_run_file_obj()) is hoisted bare and the call is kept as an assignment at its original position, specifically so the call still happens after the same preceding statements (an audit-hook check, in this case) instead of moving earlier and changing behavior.
  • A platform-conditional declaration with the same name but a different type per branch (pymain_run_startup()'s env: const wchar_t *env under #ifdef MS_WINDOWS, const char *env under #else) is hoisted under the identical #ifdef/#else/#endif structure at the top of the function, so each platform still only ever sees its own declaration.

Functions touched (8 total, 20 hoisted declarations, all in cpython/Modules/main.c): pymain_init() (preconfig, config), pymain_import_readline() (mod), pymain_run_command() (cf), pymain_run_file_obj() (fp, sb, cf, run), pymain_run_file() (filename, program_name, res), pymain_run_startup() (startup, env, fp, cf), pymain_run_stdin() (a nested-block exitcode, plus outer-scope cf, run), and pymain_repl() (cf, res). pymain_get_importer(), pymain_sys_path_add_path0(), pymain_run_module(), pymain_run_interactive_hook(), pymain_run_python(), Py_RunMain(), and pymain_main() were inspected and found to already declare every local before the first statement in their block (including two back-to-back declarations in pymain_run_python()'s else if (!config->safe_path) block, which is valid C89 since no statement separates them) — left untouched, consistent with "only the sites that actually need it."

A note on scope, since it matters for what this slice does and does not claim: several of the hoisted declarations are of type PyCompilerFlags or are assigned via _PyStatus_OK(), both of which are initialized through C99 compound-literal macros (_PyCompilerFlags_INIT, _PyStatus_OK()). Hoisting the declaration is still the correct, necessary C89 fix for the mixed-declaration violation at each of those sites — it eliminates that specific error class everywhere it appeared in main.c, as confirmed below — but the compound-literal initializer itself is a separate C99 feature VC6 does not support at any declaration position. That is a new, distinct blocker, not a residue of this slice's fix; see "Current first-blocker evidence" below, where it is documented rather than papered over.

Verified with the same two-probe approach used for the earlier slices:

  • scripts/fixtures/vc6_decl_order_smoke.c (new) — a minimal, self-contained fixture (no CPython headers) that exercises, in isolation, the shape of every hoist pattern used in main.c: a bare hoist above an early return, a side-effecting-initializer hoist, a hoisted pair where fixing the first declaration would otherwise turn an immediately-following second declaration into a new violation, a nested-block hoist, and the platform-conditional same-name/different-type hoist. Kept separate from main.c itself so it keeps guarding the general transformation shape even as main.c changes upstream:

    $ bash scripts/vc6-probe-core.sh scripts/fixtures/vc6_decl_order_smoke.c
    == MSVC600 Py_BUILD_CORE compile probe ==
    source : scripts/fixtures/vc6_decl_order_smoke.c
    ...
    vc6_decl_order_smoke.c
    
    == cl.exe exit status: 0 ==
    

    No errors, no warnings.

  • cpython/Modules/main.c itself, the same real core translation unit used throughout this document. Every C2146/C2065/C2275 declaration-position error is gone; the previous 29-error-line run is now down to 8 error lines, all a different error code (C2059) at a different root cause (compound literals / designated initializers, not declaration position — see "Current first-blocker evidence" below for the full transcript).

Both probes are covered by 5 new checks added to scripts/vc6-probe-core.test.sh (positive isolated-fixture compile, no-errors check, a negative deliberately-broken-fixture check, its error-surfaced check, and a check that Modules/main.c's output no longer contains any declaration-position error — C2146, an "undeclared identifier" C2065, or a type-as-expression C2275 — anywhere in its own body), bringing that file to 21 checks total. Run: bash scripts/vc6-probe-core.test.sh. Last run: all 21 checks ok (wine + MSVC600 available in this environment). scripts/vc6-probe.test.sh was re-run after this change and still passes (all 6 checks ok), confirming the public-header probe is unaffected. The patch's reproducibility was re-verified against a genuinely pristine tree (a fresh cp -a cpython /tmp/cpython_pristine_check && git -C /tmp/cpython_pristine_check checkout -- ., not a stash of the working checkout): git apply --check from inside that pristine copy exits 0, and after applying, diff -rq between the patched pristine copy and this project's actual cpython/ working tree (excluding .git) reports no differences. The temporary copy was removed afterward; nothing under cpython/ or compat/ was affected by this verification step.

Compound-literal / designated-initializer slice

Scope: the 8 C99 compound-literal/designated-initializer call sites inside Modules/main.c's own body only — no header-level macro change, and no other file touched. This is the follow-on to the "Modules/main.c declaration-order slice" above, addressing exactly the blocker that slice left recorded as out of scope: VC6 (a pre-C99 compiler) supports neither C99 compound literals ((PyStatus){...}) nor C99 designated initializers ({ .field = value, ... }), at any position in a block, regardless of where the destination variable is declared.

Three distinct patterns were responsible, each fixed a different way, all still uncommitted in compat/msvc600/cpython-3.11.16-vc6-headers.patch:

  • status = _PyStatus_OK(); (1 site, pymain_init()). Rather than reimplementing the macro's field assignment by hand, this is replaced with status = PyStatus_Ok(); — the existing public API function declared in Include/cpython/initconfig.h (PyAPI_FUNC(PyStatus) PyStatus_Ok(void);) and defined in Python/initconfig.c as PyStatus PyStatus_Ok(void) { return _PyStatus_OK(); } — i.e. it returns the exact value the macro would have produced, by construction, not by re-derivation. This needed no header change: the function was already declared and part of the public ABI; main.c just wasn't calling it. Behavior is identical for every compiler, VC6 or otherwise — main.c now calls a function that itself still uses the compound-literal macro internally in Python/initconfig.c, which is fine because Python/initconfig.c is not part of this slice's compile-only probe (no link is attempted or claimed) and is explicitly the next kind of file this port will need to work through, not something silently skipped.
  • cf = _PyCompilerFlags_INIT; (5 sites: pymain_run_command(), pymain_run_file_obj(), pymain_run_startup(), pymain_run_stdin(), pymain_repl()). No public-API equivalent exists for this one (unlike PyStatus_Ok()), so each site is replaced with the field-by-field assignment the macro itself expands to: cf.cf_flags = 0; cf.cf_feature_version = PY_MINOR_VERSION;. PyCompilerFlags is a two-field plain struct (Include/cpython/compile.h), and cf was already declared (bare, no initializer) at the top of its enclosing block by the prior declaration-order slice, so this is a pure compound-literal-to-assignment substitution with no interaction with declaration position.
  • _PyArgv args = { .argc = argc, ... }; (2 sites: Py_Main(), Py_BytesMain()). No macro is involved here, just a plain C99 designated-initializer aggregate initializer on a local variable declaration. Replaced with a bare declaration followed by one assignment per field (args.argc = argc; args.use_bytes_argv = 0; ...); field order in the replacement does not matter, unlike the initializer list it replaces, since each is now an independent assignment statement.

Every other compiler is unaffected: PyStatus_Ok() is a normal function call (no macro expansion difference for any compiler), and hoisting a designated-initializer aggregate into field-by-field assignment, or a compound-literal assignment into field-by-field assignment, changes no value, no evaluation order, and no code path for any compiler that already accepted the C99 form — it is strictly a syntax substitution for an identical runtime effect.

Verified with the same two-probe approach used for the earlier slices:

  • scripts/fixtures/vc6_compound_literal_smoke.c (new) — includes the real Python.h/pycore_initconfig.h (like vc6_mixed_decls_smoke.c, since PyStatus, PyCompilerFlags, and _PyArgv are real CPython types, not reproducible in isolation the way the atomics/stdbool fixtures are) and exercises all three replacement patterns:

    $ bash scripts/vc6-probe-core.sh scripts/fixtures/vc6_compound_literal_smoke.c
    == MSVC600 Py_BUILD_CORE compile probe ==
    source : scripts/fixtures/vc6_compound_literal_smoke.c
    ...
    vc6_compound_literal_smoke.c
    
    == cl.exe exit status: 0 ==
    

    No errors, no warnings.

  • cpython/Modules/main.c itself, the same real core translation unit used throughout this document. Every C2059 compound-literal/ designated-initializer error is gone — the previous 8-error-line run is now 0 error lines, and the probe reports cl.exe exit status: 0:

    $ bash scripts/vc6-probe-core.sh
    == MSVC600 Py_BUILD_CORE compile probe ==
    source : cpython/Modules/main.c
    prefix : /home/ubuntu/.wine-win9xport
    
    Setting environment for using Microsoft Visual C++ tools.
    main.c
    ...
    == cl.exe exit status: 0 ==
    

    (The only other output is MSVC600's standard informational NOTE: about WINVER 0x0500 being a beta-era SDK combination, present on every probe run in this environment, not new to this slice and not an error or warning.) This is a compile-only (/c) result for one translation unit — scripts/vc6-probe-core.sh never invokes the linker (see its own header comment) — not a claim that Modules/main.c links, that pythoncore/python.exe builds, or that anything runs on Windows 9x.

Both probes are covered by 6 new checks added to scripts/vc6-probe-core.test.sh (positive isolated-fixture compile, no-errors check, a negative deliberately-broken-fixture check, its error-surfaced check, a check that Modules/main.c's output no longer contains any C2059 compound-literal/designated-initializer error anywhere in its own body, and a new end-to-end check that the Modules/main.c probe itself now exits 0), bringing that file to 27 checks total. Run: bash scripts/vc6-probe-core.test.sh. Last run: all 27 checks ok (wine + MSVC600 available in this environment). scripts/vc6-probe.test.sh was re-run after this change and still passes (all 6 checks ok), confirming the public-header probe is unaffected. The patch's reproducibility was re-verified against a genuinely pristine tree (a fresh cp -a cpython /tmp/cpython_pristine_check && git -C /tmp/cpython_pristine_check checkout -- ., not a stash of the working checkout): git apply --check from inside that pristine copy exits 0, and after applying, diff -rq between the patched pristine copy and this project's actual cpython/ working tree (excluding .git) reports no differences. The temporary copy was removed afterward; nothing under cpython/ or compat/ was affected by this verification step.

Next blocker: Python/pylifecycle.c

After the PyRuntime section slice, the same real core probe was re-run:

$ bash scripts/vc6-probe-core.sh cpython/Python/pylifecycle.c
== MSVC600 Py_BUILD_CORE compile probe ==
source : cpython/Python/pylifecycle.c
...
cpython\Python\pylifecycle.c(112) : error C2059: syntax error : '.'
cpython\Python\pylifecycle.c(112) : error C2059: syntax error : ','
cpython\Python\pylifecycle.c(130) : error C2059: syntax error : '{'
cpython\Python\pylifecycle.c(189) : error C2143: syntax error : missing ';' before 'type'
cpython\Python\pylifecycle.c(190) : error C2143: syntax error : missing ';' before 'type'
...
cpython\Python\pylifecycle.c(1232) : fatal error C1003: error count exceeds 100; stopping compilation

== cl.exe exit status: 2 ==

The prior first blocker at Python/pylifecycle.c:91-92 is gone: VC6 no longer reports warning C4068: unknown pragma for #pragma section, and no longer rejects __declspec(allocate("PyRuntime")) with C2341. The new exact first blocker is now the _PyRuntimeState_INIT designated-initializer macro used by _PyRuntime itself (C2059: syntax error : '.' at the definition line, now line 112 after the VC6 guard was added). After that, the file still reaches the previously known broader C99 compound-literal/designated-initializer and C89 mixed-declaration wall, eventually hitting the compiler's 100-error cap. This slice did not attempt those broader pylifecycle.c rewrites.

Current first-blocker evidence

The previously recorded long long / C99 integer-suffix blocker is now resolved and verified. The uncommitted compat/msvc600/cpython-3.11.16-vc6-headers.patch (already applied to the cpython/ working tree) replaces bare long long with the existing PY_LONG_LONG macro in Include/longobject.h, Include/pythread.h, and Include/pyexpat.h, and introduces a PY_LL/PY_ULL literal-suffix macro pair (spelled i64/ui64 for _MSC_VER < 1300, i.e. VC6) in PC/pyconfig.h and Include/pyport.h, used in place of raw 123LL/123ULL literals in Include/pythread.h. With this patch applied, the full public header stack compiles cleanly:

$ bash scripts/vc6-probe.sh
== MSVC600 compile probe ==
source : cpython/Programs/python.c
prefix : /home/ubuntu/.wine-win9xport

Setting environment for using Microsoft Visual C++ tools.
python.c

== cl.exe exit status: 0 ==

cpython/Programs/python.c (#include "Python.h" plus a wmain/main stub) now compiles to build/vc6-probe/python.obj (verified present, 7115 bytes) with exit status 0. This exercises the entire public Include/*.h surface reachable from Python.h, not just pyport.h.

The pycore_atomic.h/intrin.h blocker, the pycore_interp.h/ <stdbool.h> blocker, and the mixed-declarations blocker in pycore_code.h/pycore_dict.h/pycore_list.h/pycore_call.h described in earlier revisions of this document are all now resolved; see "Atomics slice", "stdbool slice", and "Mixed-declarations header slice" above for full detail. Probing the same real core translation unit (cpython/Modules/main.c, which needs Include/internal on the include path and Py_BUILD_CORE defined — both required unconditionally by every pycore_*.h header) now gets past all of CPython's internal headers reachable from its #include chain with no header-level error at all, and the compiler proceeds into Modules/main.c's own body — where it hits a new wall, spread across nearly the entire 743-line file:

$ bash scripts/vc6-probe-core.sh
== MSVC600 Py_BUILD_CORE compile probe ==
source : cpython/Modules/main.c
...
cpython\Modules\main.c(44) : error C2275: 'PyPreConfig' : illegal use of this type as an expression
cpython\Modules\main.c(44) : error C2146: syntax error : missing ';' before identifier 'preconfig'
cpython\Modules\main.c(44) : error C2065: 'preconfig' : undeclared identifier
...
cpython\Modules\main.c(52) : error C2275: 'PyConfig' : illegal use of this type as an expression
...
cpython\Modules\main.c(215) : error C2275: 'PyObject' : illegal use of this type as an expression
...
cpython\Modules\main.c(253) : error C2275: 'PyCompilerFlags' : illegal use of this type as an expression
...
== cl.exe exit status: 2 ==

(29 distinct error source lines total in that run, spanning lines 44 through 734 of main.c — effectively the whole file — across roughly a dozen separate functions/blocks.)

This mixed-declarations pattern inside Modules/main.c's own body is now resolved and verified; see "Modules/main.c declaration-order slice" above for full detail. With that fix applied, the same probe now gets past every declaration-position error and hits a new, different, and out-of-scope-for-that-slice blocker:

$ bash scripts/vc6-probe-core.sh
== MSVC600 Py_BUILD_CORE compile probe ==
source : cpython/Modules/main.c
...
cpython\Modules\main.c(71) : error C2059: syntax error : '{'
cpython\Modules\main.c(256) : error C2059: syntax error : '{'
cpython\Modules\main.c(366) : error C2059: syntax error : '{'
cpython\Modules\main.c(448) : error C2059: syntax error : '{'
cpython\Modules\main.c(531) : error C2059: syntax error : '{'
cpython\Modules\main.c(564) : error C2059: syntax error : '{'
cpython\Modules\main.c(748) : error C2059: syntax error : '.'
cpython\Modules\main.c(760) : error C2059: syntax error : '.'

== cl.exe exit status: 2 ==

Down from 29 error lines to 8. Every one of these is the same new root cause, unrelated to declaration position: a C99 compound literal, optionally with designated initializers, neither of which VC6 (a pre-C99 compiler) supports at all, at any position in a block. Two distinct macros/patterns are responsible:

  • main.c:71, 256, 366, 448, 531, 564 — six sites assigning PyCompilerFlags cf = _PyCompilerFlags_INIT; (now cf = _PyCompilerFlags_INIT; after the declaration-order fix) or status = _PyStatus_OK();. _PyCompilerFlags_INIT and _PyStatus_OK() are defined in Include/cpython/compile.h and Include/internal/pycore_initconfig.h respectively as C99 compound literals with designated initializers, e.g.:
    #define _PyStatus_OK() \
        (PyStatus){._type = _PyStatus_TYPE_OK,}
    #define _PyCompilerFlags_INIT \
        (PyCompilerFlags){.cf_flags = 0, .cf_feature_version = PY_MINOR_VERSION}
    
    (PyStatus){...} is a compound literal (C99 §6.5.2.5); ._type = ... inside it is a designated initializer (C99 §6.7.8). VC6 recognizes neither construct and reports the same C2059: syntax error : '{' for every one, regardless of whether the assignment is the first statement in its block or not — hoisting the declaration (already done) cannot fix this, because the problem is the initializer expression's syntax, not where the variable is declared.
  • main.c:748, 760 — the two _PyArgv args = { .argc = argc, ... }; aggregate initializations in Py_Main()/Py_BytesMain(), which use plain C99 designated-initializer syntax (no compound-literal macro involved, just { .field = value, ... } directly in a local variable's initializer) for the same reason: VC6 predates C99 designated initializers entirely.

This is a distinct C99 language-feature gap from mixed declarations, narrower in one sense (it doesn't force auditing every block in the file the way declaration-position did) but not fixable by moving code around: the fix, when attempted, will need to replace each compound-literal initializer with explicit field-by-field assignment statements (e.g. cf.cf_flags = 0; cf.cf_feature_version = PY_MINOR_VERSION;) or an equivalent non-compound-literal C89 construct, and — for _PyStatus_OK() / _PyCompilerFlags_INIT specifically — probably wants a header-level macro change (in pycore_initconfig.h / cpython/compile.h) analogous to the _Py_ATOMIC_VC6_X86 gating already used for pycore_atomic.h, rather than a per-call-site rewrite in every .c file that uses these macros, since they are used far more broadly than just Modules/main.c. No such fix is attempted here; it is out of scope for this narrowly-scoped declaration-order pass and is recorded as the next blocker. No link or runtime success is claimed at this or any earlier point in this document.

Update: this blocker (all 8 C2059 sites above) is now resolved for Modules/main.c, via a per-call-site rewrite rather than the header-level macro change speculated above — see "Compound-literal / designated-initializer slice" above for what was actually done and why the per-call-site approach (not the header macro change) was chosen for this narrow slice, and "Next blocker: Python/pylifecycle.c" above for where the same two blocker classes (mixed declarations and compound literals/designated initializers) recur next. This "Current first-blocker evidence" section is kept as a historical record of how the diagnosis proceeded; it is no longer the current state.

Larger blockers to plan for

  1. Build-system mismatch: CPython 3.11's Windows build expects Visual Studio 2017-era MSBuild/vcxproj tooling. VC6 cannot consume the build as-is. A dedicated minimal VC6 makefile/project set is required.
  2. Compiler language gap: CPython 3.11 source and headers use C99-era and later MSVC features that VC6 does not implement. Compatibility macros and carefully scoped source backports are required.
    • VC6's C89-only parser rejects CPython's pervasive C99-style mixed declarations-and-code. The instances of this pattern in four Include/internal/pycore_*.h headers reachable from Modules/main.c are fixed (see "Mixed-declarations header slice" above), and the same pattern recurring throughout Modules/main.c's own body (29 error sites across roughly a dozen functions) is also now fixed (see "Modules/main.c declaration-order slice" above). Whether this pattern recurs in CPython's other .c sources has not yet been surveyed; each file would need its own probe-and-fix pass the same way main.c did, not a whole-tree sweep in one change.
    • VC6 does not support C99 compound literals or designated initializers at all, independent of declaration position. The 8 sites of this pattern in Modules/main.c itself (_PyStatus_OK(), _PyCompilerFlags_INIT, and inline { .field = value } initialization in Py_Main()/Py_BytesMain()) are now fixed via a per-call-site rewrite, not a header-level macro change (see "Compound-literal / designated-initializer slice" above for why: a public-API function already existed for _PyStatus_OK(), and the other two patterns are cheap to rewrite in place without touching shared headers used far more broadly than main.c). _PyStatus_OK() and _PyCompilerFlags_INIT are still used, unmodified, at many other call sites across CPython outside main.c — this fix is scoped to main.c only, as directed; a header-level VC6 branch (mirroring _Py_ATOMIC_VC6_X86) remains a live option if a future slice finds the per-call-site approach doesn't scale to the rest of the codebase. The same two patterns (mixed declarations and compound literals) both recur in Python/pylifecycle.c, the next core translation unit in main.c's own call chain; see "Next blocker: Python/pylifecycle.c" above. Not yet fixed there.
  3. Win9x API gap: CPython 3.11's Windows layer assumes NT-family Unicode, synchronization, process, filesystem, and networking APIs. Windows 98 has no wide-character file APIs and lacks multiple newer kernel APIs.
  4. Runtime/library gap: SSL, SQLite, ctypes, multiprocessing, asyncio, Unicode filesystem behavior, and parts of subprocess will need explicit feature gates or Win9x-specific implementations.
  5. Validation gap: final success requires booting the built executable on Windows 98 SE; Wine compilation alone is not proof of Win9x runtime compatibility.

Do not attempt the complete CPython solution yet. The public header surface compiles; Include/internal/pycore_atomic.h compiles for VC6/_M_IX86 (see "Atomics slice" above); Include/internal/pycore_interp.h's <stdbool.h> include now resolves via the project-local compat/msvc600/stdbool.h shim (see "stdbool slice" above); the C99 mixed-declarations pattern in the four pycore_*.h headers reachable from Modules/main.c is fixed (see "Mixed-declarations header slice" above); that same pattern inside Modules/main.c's own body is fixed (see "Modules/main.c declaration-order slice" above); and the C99 compound-literal/designated-initializer pattern inside Modules/main.c's own body is now also fixed (see "Compound-literal / designated-initializer slice" above). cpython/Modules/main.c now compiles with cl.exe exit status 0 under scripts/vc6-probe-core.sh — no errors, no warnings — a compile-only (/c) result for this one translation unit; no link or runtime claim is made.

The next blocker is still Python/pylifecycle.c, the next core translation unit in main.c's own call chain (via Py_InitializeFromConfig()), but it has advanced past the VC6-incompatible PyRuntime section pragma. See "Next blocker: Python/pylifecycle.c" above for the current probe transcript. It now hits the same two already-solved blocker classes recurring at greater scale (mixed declarations, compound literals/designated initializers), plus enough sites of both to hit the compiler's 100-error cap before the whole file is seen, so the true scope is not yet known. Recommended next steps:

  1. Continue with Python/pylifecycle.c by applying the same two mechanical transforms already validated on Modules/main.c — hoist mid-block declarations (declaration-order slice) and replace compound-literal/ designated-initializer assignments with field-by-field assignment or, where a public API function already exists (as PyStatus_Ok() did for _PyStatus_OK()), a function call (compound-literal slice) — fixing what the compiler surfaces in batches of roughly 100 (its per-run error cap) and re-probing between batches, since the full error count for this file is not yet known.
  2. Re-evaluate the header-level-macro-vs-per-call-site tradeoff noted in "Larger blockers to plan for" above once _PyStatus_OK()/ _PyCompilerFlags_INIT-style macros have been rewritten by hand in two files (main.c and pylifecycle.c): if the same macros keep recurring across many more .c files beyond these two, a header-level VC6 branch (mirroring _Py_ATOMIC_VC6_X86 in "Atomics slice" above) becomes more attractive than continuing per-call-site rewrites file by file.
  3. Only after enough core translation units compile, and a minimal pythoncore/Programs/python.c link is attempted, should work move to the Win9x API surface one subsystem at a time.

Validate incrementally with scripts/vc6-probe-core.sh against each file being worked on, the same way this and prior slices did.

Scope boundary

This project does not vendor the 223 MB MSVC600 bundle into a distributable repository. It records the external tool as a local build prerequisite. No Microsoft compiler binaries or runtime files should be committed or published.