From a7a9560e5c53b3d1af77bb3d8956623e6dc877bd Mon Sep 17 00:00:00 2001 From: RayPals Date: Mon, 17 Aug 2026 17:35:32 +0000 Subject: [PATCH] port: add MSVC600 CPython compatibility layer --- PORT_STATUS.md | 892 +++++++++++++++++- README.md | 19 +- .../msvc600/cpython-3.11.16-vc6-headers.patch | 739 ++++++++++++++- compat/msvc600/stdbool.h | 35 + scripts/fixtures/vc6_atomic_smoke.c | 36 + scripts/fixtures/vc6_compound_literal_smoke.c | 64 ++ scripts/fixtures/vc6_decl_order_smoke.c | 162 ++++ scripts/fixtures/vc6_mixed_decls_smoke.c | 54 ++ scripts/fixtures/vc6_stdbool_smoke.c | 31 + scripts/vc6-probe-core.sh | 104 ++ scripts/vc6-probe-core.test.sh | 337 +++++++ scripts/vc6-probe.sh | 107 +++ scripts/vc6-probe.test.sh | 107 +++ 13 files changed, 2652 insertions(+), 35 deletions(-) create mode 100644 compat/msvc600/stdbool.h create mode 100644 scripts/fixtures/vc6_atomic_smoke.c create mode 100644 scripts/fixtures/vc6_compound_literal_smoke.c create mode 100644 scripts/fixtures/vc6_decl_order_smoke.c create mode 100644 scripts/fixtures/vc6_mixed_decls_smoke.c create mode 100644 scripts/fixtures/vc6_stdbool_smoke.c create mode 100755 scripts/vc6-probe-core.sh create mode 100755 scripts/vc6-probe-core.test.sh create mode 100755 scripts/vc6-probe.sh create mode 100755 scripts/vc6-probe.test.sh diff --git a/PORT_STATUS.md b/PORT_STATUS.md index 94f11c2..0aceaaa 100644 --- a/PORT_STATUS.md +++ b/PORT_STATUS.md @@ -5,6 +5,36 @@ 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. @@ -19,33 +49,778 @@ Host test environment: Linux + Wine - 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. -- A project-local compatibility `inttypes.h` has been added under - `compat/msvc600/` as the first isolated shim. It is not yet wired into - CPython source or declared sufficient for the port. +- 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/`` 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: + +```c +#if defined(_MSC_VER) && _MSC_VER < 1300 && !defined(__func__) +# define __func__ "" +#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 +`` 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 ``/`` include is skipped for + VC6/`_M_IX86`; `` 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 `` + 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`: + + ```text + $ 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: + + ```text + $ 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 ` + 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 ``) + +**`Include/internal/pycore_interp.h` line 11 does `#include `** +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 `` 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 ` 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 `` (or built-in `_Bool`/`bool`), untouched. +As defence in depth (not because it is expected to be reachable), the +header itself also `#error`s 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 `` and exercises + `bool`/`true`/`false`/`__bool_true_false_are_defined`: + + ```text + $ 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: + + ```text + $ 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: + + ```text + $ 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: + + ```text + $ 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`: + + ```text + $ 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: + +```text +$ 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 probe command was equivalent to: +**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: ```text -cl /nologo /I compat/msvc600 /I cpython/Include /I cpython/PC \ - /c cpython/Programs/python.c +$ 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 == ``` -The next errors include: +`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`. -- `Py_uintptr_t` / `Py_intptr_t` configuration failures in `Include/pyport.h` -- unsupported C `inline` syntax in `Include/object.h` and other public headers -- unsupported `__declspec(deprecated)` annotations -- additional C99/C99-era declarations and compatibility assumptions +**The `pycore_atomic.h`/`intrin.h` blocker, the `pycore_interp.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: -The compatibility header now supplies the pointer-sized typedefs required by -`pyport.h`; the current first compiler diagnostics after applying the saved -`compat/msvc600/cpython-3.11.16-vc6-headers.patch` are VC6's inability to parse -`long long` declarations in `Include/longobject.h` and C99-style integer -literal suffixes in `Include/pythread.h`. The shim and patch resolve only the -missing-header, pointer-type, inline, and deprecation layers; they are -intentionally not presented as a complete fix. +```text +$ 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: + +```text +$ 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.: + ```c + #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 @@ -55,6 +830,35 @@ intentionally not presented as a complete fix. 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. @@ -67,11 +871,53 @@ intentionally not presented as a complete fix. ## Recommended next implementation slice -Do not attempt the complete CPython solution yet. First create a minimal VC6 -build target for `pythoncore` plus `Programs/python.c`, then make the public -headers compile under VC6 through a clearly isolated compatibility layer. Keep -all changes small and testable. After the core links, address the Win9x API -surface one subsystem at a time. +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 +`` 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 diff --git a/README.md b/README.md index 02ab917..7675b83 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,18 @@ The CPython baseline is kept in `cpython/` at tag `v3.11.16`. The MSVC600 bundle is kept in `MSVC600/` as a local external prerequisite and is not part of the distributable project source. -The first VC6 compiler probe succeeds under Wine. A direct compile of -`cpython/Programs/python.c` currently fails in CPython headers because VC6 -lacks or does not understand several facilities used by CPython 3.11: +The public-header probe and a real `Py_BUILD_CORE` translation unit now compile +under MSVC600 through the project compatibility layer: -- C99 `inttypes.h` and related integer-format definitions -- C99 `inline` syntax in C mode -- newer `__declspec(deprecated)` annotations -- modern pointer-width/type configuration expected by `pyport.h` -- the CPython 3.11 Visual Studio/MSBuild project model +- `cpython/Programs/python.c` compiles successfully. +- `cpython/Modules/main.c` compiles successfully. +- `cpython/Python/pylifecycle.c` is the current larger-core probe and remains + blocked by CPython 3.11 C99 initializers, mixed declarations, and additional + legacy-compiler/platform differences. -These are expected porting blockers and are recorded in `PORT_STATUS.md`. +This is still an experimental porting workspace, not a completed Windows 98 +build. Exact verified milestones and blockers are recorded in +`PORT_STATUS.md`. ## Toolchain policy diff --git a/compat/msvc600/cpython-3.11.16-vc6-headers.patch b/compat/msvc600/cpython-3.11.16-vc6-headers.patch index e73e571..42b7501 100644 --- a/compat/msvc600/cpython-3.11.16-vc6-headers.patch +++ b/compat/msvc600/cpython-3.11.16-vc6-headers.patch @@ -1,8 +1,356 @@ +diff --git a/Include/internal/pycore_atomic.h b/Include/internal/pycore_atomic.h +index 425d69f..748de28 100644 +--- a/Include/internal/pycore_atomic.h ++++ b/Include/internal/pycore_atomic.h +@@ -16,7 +16,21 @@ extern "C" { + #endif + + +-#if defined(_MSC_VER) ++/* Visual C++ 6.0 (_MSC_VER 1200) predates and the _Interlocked* ++ * compiler intrinsics (both introduced in Visual Studio 2005, _MSC_VER ++ * 1400) as well as . This port only targets 32-bit x86 under ++ * VC6, so gate the modern-MSVC include on that combination and give VC6/x86 ++ * a project-local branch below (_Py_ATOMIC_VC6_X86) built on the Win32 ++ * Interlocked* API instead. Every other compiler/version keeps the ++ * unmodified upstream behavior. ++ */ ++#if defined(_MSC_VER) && _MSC_VER < 1300 && defined(_M_IX86) ++# define _Py_ATOMIC_VC6_X86 1 ++ /* Declares InterlockedExchange()/InterlockedCompareExchange(), used by ++ * the _Py_ATOMIC_VC6_X86 branch below in place of the _Interlocked* ++ * intrinsics the modern-MSVC branch gets from . */ ++# include ++#elif defined(_MSC_VER) + #include + #if defined(_M_IX86) || defined(_M_X64) + # include +@@ -238,7 +252,87 @@ _Py_ANNOTATE_MEMORY_ORDER(const volatile void *address, _Py_memory_order order) + This might affect performance but likely only in some very specific and + hard to measure scenario. + */ +-#if defined(_M_IX86) || defined(_M_X64) ++#if defined(_Py_ATOMIC_VC6_X86) ++/* VC6 / Windows 9x, 32-bit x86 only (_M_X64 is never defined by VC6). ++ VC6 has neither /_Interlocked* compiler intrinsics (Visual ++ Studio 2005+) nor the _HLEAcquire/_HLERelease hardware-lock-elision ++ variants (Visual Studio 2013+) used by the modern-MSVC branch below. ++ Substitute the plain Win32 InterlockedExchange/InterlockedCompareExchange ++ functions, which are exported by KERNEL32.DLL on Windows 98 (this VC6 ++ SDK's WINBASE.H declares them for _M_IX86 as compiler-intrinsic-backed ++ APIs, not the newer MRx000/Alpha-only intrinsic path above them in that ++ header). Like the modern-MSVC intrinsics, these give a full memory ++ barrier, so -- as with the HLE fallback described above -- every ++ _Py_memory_order maps to the same fully-fenced operation; there is no ++ separate acquire/release-only path to take. ++ ++ _Py_atomic_address (uintptr_t) and _Py_atomic_int (int) are both 4 ++ bytes on this 32-bit-only target, so the 64-bit store/load paths used ++ by the modern-MSVC branch below (guarded there by `sizeof(...) == 8`) ++ are unreachable here and are intentionally not implemented: this port ++ has no Win9x-era substitute for CMPXCHG8B-based 64-bit atomics. Fail ++ loudly at compile time rather than silently mishandling a 64-bit ++ target if this header is ever pulled into a VC6 build configured for ++ one (VC6 itself never defines _M_X64, so this should not trigger). ++*/ ++#if defined(_M_X64) ++#error "VC6 64-bit atomics are not implemented; this port only targets 32-bit x86 (_M_IX86)." ++#endif ++ ++typedef enum _Py_memory_order { ++ _Py_memory_order_relaxed, ++ _Py_memory_order_acquire, ++ _Py_memory_order_release, ++ _Py_memory_order_acq_rel, ++ _Py_memory_order_seq_cst ++} _Py_memory_order; ++ ++typedef struct _Py_atomic_address { ++ volatile uintptr_t _value; ++} _Py_atomic_address; ++ ++typedef struct _Py_atomic_int { ++ volatile int _value; ++} _Py_atomic_int; ++ ++static __inline long ++_Py_vc6_InterlockedExchange(volatile long *target, long value) ++{ ++ return InterlockedExchange((LPLONG)target, value); ++} ++ ++static __inline long ++_Py_vc6_InterlockedCompareExchange(volatile long *destination, long exchange, long comperand) ++{ ++ return (long)InterlockedCompareExchange( ++ (PVOID *)destination, (PVOID)exchange, (PVOID)comperand); ++} ++ ++#define _Py_atomic_store_32bit(ATOMIC_VAL, NEW_VAL, ORDER) \ ++ ((void)_Py_vc6_InterlockedExchange( \ ++ (volatile long *)&((ATOMIC_VAL)->_value), (long)(NEW_VAL))) ++ ++static __inline int ++_Py_atomic_load_32bit_impl(volatile int *value, int order) ++{ ++ long old; ++ (void)order; ++ do { ++ old = *(volatile long *)value; ++ } while (_Py_vc6_InterlockedCompareExchange((volatile long *)value, old, old) != old); ++ return (int)old; ++} ++ ++#define _Py_atomic_load_32bit(ATOMIC_VAL, ORDER) \ ++ _Py_atomic_load_32bit_impl((volatile int *)&((ATOMIC_VAL)->_value), (ORDER)) ++ ++#define _Py_atomic_store_explicit(ATOMIC_VAL, NEW_VAL, ORDER) \ ++ _Py_atomic_store_32bit((ATOMIC_VAL), (NEW_VAL), (ORDER)) ++ ++#define _Py_atomic_load_explicit(ATOMIC_VAL, ORDER) \ ++ _Py_atomic_load_32bit((ATOMIC_VAL), (ORDER)) ++ ++#elif defined(_M_IX86) || defined(_M_X64) + typedef enum _Py_memory_order { + _Py_memory_order_relaxed, + _Py_memory_order_acquire, +diff --git a/Include/internal/pycore_call.h b/Include/internal/pycore_call.h +index 3ccacfa..3098e1e 100644 +--- a/Include/internal/pycore_call.h ++++ b/Include/internal/pycore_call.h +@@ -38,18 +38,21 @@ extern PyObject * _PyObject_CallMethodFormat( + static inline vectorcallfunc + _PyVectorcall_FunctionInline(PyObject *callable) + { ++ PyTypeObject *tp; ++ Py_ssize_t offset; ++ vectorcallfunc ptr; ++ + assert(callable != NULL); + +- PyTypeObject *tp = Py_TYPE(callable); ++ tp = Py_TYPE(callable); + if (!PyType_HasFeature(tp, Py_TPFLAGS_HAVE_VECTORCALL)) { + return NULL; + } + assert(PyCallable_Check(callable)); + +- Py_ssize_t offset = tp->tp_vectorcall_offset; ++ offset = tp->tp_vectorcall_offset; + assert(offset > 0); + +- vectorcallfunc ptr; + memcpy(&ptr, (char *) callable + offset, sizeof(ptr)); + return ptr; + } +diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h +index 3a24a65..4db8abc 100644 +--- a/Include/internal/pycore_code.h ++++ b/Include/internal/pycore_code.h +@@ -149,18 +149,20 @@ typedef unsigned char _PyLocals_Kind; + static inline _PyLocals_Kind + _PyLocals_GetKind(PyObject *kinds, int i) + { ++ char *ptr; + assert(PyBytes_Check(kinds)); + assert(0 <= i && i < PyBytes_GET_SIZE(kinds)); +- char *ptr = PyBytes_AS_STRING(kinds); ++ ptr = PyBytes_AS_STRING(kinds); + return (_PyLocals_Kind)(ptr[i]); + } + + static inline void + _PyLocals_SetKind(PyObject *kinds, int i, _PyLocals_Kind kind) + { ++ char *ptr; + assert(PyBytes_Check(kinds)); + assert(0 <= i && i < PyBytes_GET_SIZE(kinds)); +- char *ptr = PyBytes_AS_STRING(kinds); ++ ptr = PyBytes_AS_STRING(kinds); + ptr[i] = (char) kind; + } + +@@ -520,11 +522,12 @@ adaptive_counter_start(void) { + static inline uint16_t + adaptive_counter_backoff(uint16_t counter) { + unsigned int backoff = counter & ((1< MAX_BACKOFF_VALUE) { + backoff = MAX_BACKOFF_VALUE; + } +- unsigned int value = (1 << backoff) - 1; ++ value = (1 << backoff) - 1; + return adaptive_counter_bits(value, backoff); + } + +diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h +index dc308fe..16feaf3 100644 +--- a/Include/internal/pycore_dict.h ++++ b/Include/internal/pycore_dict.h +@@ -163,9 +163,11 @@ extern PyObject *_PyDict_FromItems( + static inline void + _PyDictValues_AddToInsertionOrder(PyDictValues *values, Py_ssize_t ix) + { ++ uint8_t *size_ptr; ++ int size; + assert(ix < SHARED_KEYS_MAX_SIZE); +- uint8_t *size_ptr = ((uint8_t *)values)-2; +- int size = *size_ptr; ++ size_ptr = ((uint8_t *)values)-2; ++ size = *size_ptr; + assert(size+2 < ((uint8_t *)values)[-1]); + size++; + size_ptr[-size] = (uint8_t)ix; +diff --git a/Include/internal/pycore_list.h b/Include/internal/pycore_list.h +index 860dce1..da38ee6 100644 +--- a/Include/internal/pycore_list.h ++++ b/Include/internal/pycore_list.h +@@ -43,10 +43,12 @@ _PyList_AppendTakeRefListResize(PyListObject *self, PyObject *newitem); + static inline int + _PyList_AppendTakeRef(PyListObject *self, PyObject *newitem) + { ++ Py_ssize_t len; ++ Py_ssize_t allocated; + assert(self != NULL && newitem != NULL); + assert(PyList_Check(self)); +- Py_ssize_t len = PyList_GET_SIZE(self); +- Py_ssize_t allocated = self->allocated; ++ len = PyList_GET_SIZE(self); ++ allocated = self->allocated; + assert((size_t)len + 1 < PY_SSIZE_T_MAX); + if (allocated > len) { + PyList_SET_ITEM(self, len, newitem); +diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h +index f022f82..d1c55ae 100644 +--- a/Include/internal/pycore_object.h ++++ b/Include/internal/pycore_object.h +@@ -14,6 +14,14 @@ extern "C" { + #include "pycore_pystate.h" // _PyInterpreterState_GET() + #include "pycore_runtime.h" // _PyRuntime + ++/* Visual C++ 6.0 predates C99's __func__ predefined identifier. It also ++ lacks MSVC's later __FUNCTION__ extension, so provide a VC6-only fallback ++ for debug/fatal diagnostics in this header without changing any other ++ compiler's function-name reporting. */ ++#if defined(_MSC_VER) && _MSC_VER < 1300 && !defined(__func__) ++# define __func__ "" ++#endif ++ + #define _PyObject_IMMORTAL_INIT(type) \ + { \ + .ob_refcnt = 999999999, \ +@@ -125,19 +133,24 @@ static inline void _PyObject_GC_TRACK( + #endif + PyObject *op) + { ++ PyGC_Head *gc; ++ PyInterpreterState *interp; ++ PyGC_Head *generation0; ++ PyGC_Head *last; ++ + _PyObject_ASSERT_FROM(op, !_PyObject_GC_IS_TRACKED(op), + "object already tracked by the garbage collector", + filename, lineno, __func__); + +- PyGC_Head *gc = _Py_AS_GC(op); ++ gc = _Py_AS_GC(op); + _PyObject_ASSERT_FROM(op, + (gc->_gc_prev & _PyGC_PREV_MASK_COLLECTING) == 0, + "object is in generation which is garbage collected", + filename, lineno, __func__); + +- PyInterpreterState *interp = _PyInterpreterState_GET(); +- PyGC_Head *generation0 = interp->gc.generation0; +- PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev); ++ interp = _PyInterpreterState_GET(); ++ generation0 = interp->gc.generation0; ++ last = (PyGC_Head*)(generation0->_gc_prev); + _PyGCHead_SET_NEXT(last, gc); + _PyGCHead_SET_PREV(gc, last); + _PyGCHead_SET_NEXT(gc, generation0); +@@ -161,13 +174,17 @@ static inline void _PyObject_GC_UNTRACK( + #endif + PyObject *op) + { ++ PyGC_Head *gc; ++ PyGC_Head *prev; ++ PyGC_Head *next; ++ + _PyObject_ASSERT_FROM(op, _PyObject_GC_IS_TRACKED(op), + "object not tracked by the garbage collector", + filename, lineno, __func__); + +- PyGC_Head *gc = _Py_AS_GC(op); +- PyGC_Head *prev = _PyGCHead_PREV(gc); +- PyGC_Head *next = _PyGCHead_NEXT(gc); ++ gc = _Py_AS_GC(op); ++ prev = _PyGCHead_PREV(gc); ++ next = _PyGCHead_NEXT(gc); + _PyGCHead_SET_NEXT(prev, next); + _PyGCHead_SET_PREV(next, prev); + gc->_gc_next = 0; +diff --git a/Include/longobject.h b/Include/longobject.h +index 778cb62..d25edf7 100644 +--- a/Include/longobject.h ++++ b/Include/longobject.h +@@ -81,12 +81,12 @@ PyAPI_FUNC(double) PyLong_AsDouble(PyObject *); + PyAPI_FUNC(PyObject *) PyLong_FromVoidPtr(void *); + PyAPI_FUNC(void *) PyLong_AsVoidPtr(PyObject *); + +-PyAPI_FUNC(PyObject *) PyLong_FromLongLong(long long); +-PyAPI_FUNC(PyObject *) PyLong_FromUnsignedLongLong(unsigned long long); +-PyAPI_FUNC(long long) PyLong_AsLongLong(PyObject *); +-PyAPI_FUNC(unsigned long long) PyLong_AsUnsignedLongLong(PyObject *); +-PyAPI_FUNC(unsigned long long) PyLong_AsUnsignedLongLongMask(PyObject *); +-PyAPI_FUNC(long long) PyLong_AsLongLongAndOverflow(PyObject *, int *); ++PyAPI_FUNC(PyObject *) PyLong_FromLongLong(PY_LONG_LONG); ++PyAPI_FUNC(PyObject *) PyLong_FromUnsignedLongLong(unsigned PY_LONG_LONG); ++PyAPI_FUNC(PY_LONG_LONG) PyLong_AsLongLong(PyObject *); ++PyAPI_FUNC(unsigned PY_LONG_LONG) PyLong_AsUnsignedLongLong(PyObject *); ++PyAPI_FUNC(unsigned PY_LONG_LONG) PyLong_AsUnsignedLongLongMask(PyObject *); ++PyAPI_FUNC(PY_LONG_LONG) PyLong_AsLongLongAndOverflow(PyObject *, int *); + + PyAPI_FUNC(PyObject *) PyLong_FromString(const char *, char **, int); + +diff --git a/Include/pyexpat.h b/Include/pyexpat.h +index a676e16..017e86d 100644 +--- a/Include/pyexpat.h ++++ b/Include/pyexpat.h +@@ -54,12 +54,12 @@ struct PyExpat_CAPI + XML_Bool (*SetReparseDeferralEnabled)(XML_Parser parser, XML_Bool enabled); + /* might be NULL for expat < 2.7.2 */ + XML_Bool (*SetAllocTrackerActivationThreshold)( +- XML_Parser parser, unsigned long long activationThresholdBytes); ++ XML_Parser parser, unsigned PY_LONG_LONG activationThresholdBytes); + XML_Bool (*SetAllocTrackerMaximumAmplification)( + XML_Parser parser, float maxAmplificationFactor); + /* might be NULL for expat < 2.4.0 */ + XML_Bool (*SetBillionLaughsAttackProtectionActivationThreshold)( +- XML_Parser parser, unsigned long long activationThresholdBytes); ++ XML_Parser parser, unsigned PY_LONG_LONG activationThresholdBytes); + XML_Bool (*SetBillionLaughsAttackProtectionMaximumAmplification)( + XML_Parser parser, float maxAmplificationFactor); + /* might be NULL for expat < 2.8.0 */ diff --git a/Include/pyport.h b/Include/pyport.h -index b3b8b6f..4da45ef 100644 +index b3b8b6f..ae930fc 100644 --- a/Include/pyport.h +++ b/Include/pyport.h -@@ -334,9 +334,12 @@ extern "C" { +@@ -97,6 +97,13 @@ Used in: Py_SAFE_DOWNCAST + #define PY_ULLONG_MAX ULLONG_MAX + #endif + ++/* Spelling of 64 bit integer literal suffixes; see PC/pyconfig.h for the ++ compilers that need something other than the C99 LL/ULL suffixes. */ ++#ifndef PY_LL ++#define PY_LL(x) x##LL ++#define PY_ULL(x) x##ULL ++#endif ++ + #define PY_UINT32_T uint32_t + #define PY_UINT64_T uint64_t + +@@ -334,9 +341,12 @@ extern "C" { #if defined(__GNUC__) \ && ((__GNUC__ >= 4) || (__GNUC__ == 3) && (__GNUC_MINOR__ >= 1)) #define Py_DEPRECATED(VERSION_UNUSED) __attribute__((__deprecated__)) @@ -16,7 +364,7 @@ index b3b8b6f..4da45ef 100644 #else #define Py_DEPRECATED(VERSION_UNUSED) #endif -@@ -352,10 +355,14 @@ extern "C" { +@@ -352,10 +362,14 @@ extern "C" { #define _Py_COMP_DIAG_IGNORE_DEPR_DECLS \ _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") #define _Py_COMP_DIAG_POP _Pragma("GCC diagnostic pop") @@ -32,3 +380,388 @@ index b3b8b6f..4da45ef 100644 #else #define _Py_COMP_DIAG_PUSH #define _Py_COMP_DIAG_IGNORE_DEPR_DECLS +diff --git a/Include/pythread.h b/Include/pythread.h +index a483290..defdaf1 100644 +--- a/Include/pythread.h ++++ b/Include/pythread.h +@@ -40,23 +40,23 @@ PyAPI_FUNC(int) PyThread_acquire_lock(PyThread_type_lock, int); + module exposes a higher-level API, with timeouts expressed in seconds + and floating-point numbers allowed. + */ +-#define PY_TIMEOUT_T long long ++#define PY_TIMEOUT_T PY_LONG_LONG + + #if defined(_POSIX_THREADS) + /* PyThread_acquire_lock_timed() uses _PyTime_FromNanoseconds(us * 1000), + convert microseconds to nanoseconds. */ +-# define PY_TIMEOUT_MAX (LLONG_MAX / 1000) ++# define PY_TIMEOUT_MAX (PY_LLONG_MAX / 1000) + #elif defined (NT_THREADS) + // WaitForSingleObject() accepts timeout in milliseconds in the range + // [0; 0xFFFFFFFE] (DWORD type). INFINITE value (0xFFFFFFFF) means no + // timeout. 0xFFFFFFFE milliseconds is around 49.7 days. +-# if 0xFFFFFFFELL * 1000 < LLONG_MAX +-# define PY_TIMEOUT_MAX (0xFFFFFFFELL * 1000) ++# if PY_LL(0xFFFFFFFE) * 1000 < PY_LLONG_MAX ++# define PY_TIMEOUT_MAX (PY_LL(0xFFFFFFFE) * 1000) + # else +-# define PY_TIMEOUT_MAX LLONG_MAX ++# define PY_TIMEOUT_MAX PY_LLONG_MAX + # endif + #else +-# define PY_TIMEOUT_MAX LLONG_MAX ++# define PY_TIMEOUT_MAX PY_LLONG_MAX + #endif + + +diff --git a/Modules/main.c b/Modules/main.c +index f6dade1..011ae59 100644 +--- a/Modules/main.c ++++ b/Modules/main.c +@@ -35,13 +35,14 @@ static PyStatus + pymain_init(const _PyArgv *args) + { + PyStatus status; ++ PyPreConfig preconfig; ++ PyConfig config; + + status = _PyRuntime_Initialize(); + if (_PyStatus_EXCEPTION(status)) { + return status; + } + +- PyPreConfig preconfig; + PyPreConfig_InitPythonConfig(&preconfig); + + status = _Py_PreInitializeFromPyArgv(&preconfig, args); +@@ -49,7 +50,6 @@ pymain_init(const _PyArgv *args) + return status; + } + +- PyConfig config; + PyConfig_InitPythonConfig(&config); + + /* pass NULL as the config: config is read from command line arguments, +@@ -68,7 +68,13 @@ pymain_init(const _PyArgv *args) + if (_PyStatus_EXCEPTION(status)) { + goto done; + } +- status = _PyStatus_OK(); ++ /* VC6 (MSVC600) has no C99 compound literals, so _PyStatus_OK() (a ++ compound literal macro) cannot be used here; PyStatus_Ok() is the ++ public API function that returns the identical value ++ (Python/initconfig.c: `PyStatus PyStatus_Ok(void) { return ++ _PyStatus_OK(); }`), so this is not a behavior change for any ++ compiler. */ ++ status = PyStatus_Ok(); + + done: + PyConfig_Clear(&config); +@@ -202,6 +208,8 @@ pymain_header(const PyConfig *config) + static void + pymain_import_readline(const PyConfig *config) + { ++ PyObject *mod; ++ + if (config->isolated) { + return; + } +@@ -212,7 +220,7 @@ pymain_import_readline(const PyConfig *config) + return; + } + +- PyObject *mod = PyImport_ImportModule("readline"); ++ mod = PyImport_ImportModule("readline"); + if (mod == NULL) { + PyErr_Clear(); + } +@@ -234,6 +242,7 @@ pymain_run_command(wchar_t *command) + { + PyObject *unicode, *bytes; + int ret; ++ PyCompilerFlags cf; + + unicode = PyUnicode_FromWideChar(command, -1); + if (unicode == NULL) { +@@ -250,7 +259,12 @@ pymain_run_command(wchar_t *command) + goto error; + } + +- PyCompilerFlags cf = _PyCompilerFlags_INIT; ++ /* VC6 has no C99 compound literals/designated initializers, so ++ _PyCompilerFlags_INIT (`(PyCompilerFlags){.cf_flags = 0, ++ .cf_feature_version = PY_MINOR_VERSION}`) cannot be used as an ++ initializer here; assign the same two fields explicitly instead. */ ++ cf.cf_flags = 0; ++ cf.cf_feature_version = PY_MINOR_VERSION; + cf.cf_flags |= PyCF_IGNORE_COOKIE; + ret = PyRun_SimpleStringFlags(PyBytes_AsString(bytes), &cf); + Py_DECREF(bytes); +@@ -317,11 +331,16 @@ static int + pymain_run_file_obj(PyObject *program_name, PyObject *filename, + int skip_source_first_line) + { ++ FILE *fp; ++ struct _Py_stat_struct sb; ++ PyCompilerFlags cf; ++ int run; ++ + if (PySys_Audit("cpython.run_file", "O", filename) < 0) { + return pymain_exit_err_print(); + } + +- FILE *fp = _Py_fopen_obj(filename, "rb"); ++ fp = _Py_fopen_obj(filename, "rb"); + if (fp == NULL) { + // Ignore the OSError + PyErr_Clear(); +@@ -341,7 +360,6 @@ pymain_run_file_obj(PyObject *program_name, PyObject *filename, + } + } + +- struct _Py_stat_struct sb; + if (_Py_fstat_noraise(fileno(fp), &sb) == 0 && S_ISDIR(sb.st_mode)) { + PySys_FormatStderr("%S: %R is a directory, cannot continue\n", + program_name, filename); +@@ -356,28 +374,35 @@ pymain_run_file_obj(PyObject *program_name, PyObject *filename, + } + + /* PyRun_AnyFileExFlags(closeit=1) calls fclose(fp) before running code */ +- PyCompilerFlags cf = _PyCompilerFlags_INIT; +- int run = _PyRun_AnyFileObject(fp, filename, 1, &cf); ++ /* See pymain_run_command() above for why this is not ++ _PyCompilerFlags_INIT (VC6 has no compound literals). */ ++ cf.cf_flags = 0; ++ cf.cf_feature_version = PY_MINOR_VERSION; ++ run = _PyRun_AnyFileObject(fp, filename, 1, &cf); + return (run != 0); + } + + static int + pymain_run_file(const PyConfig *config) + { +- PyObject *filename = PyUnicode_FromWideChar(config->run_filename, -1); ++ PyObject *filename; ++ PyObject *program_name; ++ int res; ++ ++ filename = PyUnicode_FromWideChar(config->run_filename, -1); + if (filename == NULL) { + PyErr_Print(); + return -1; + } +- PyObject *program_name = PyUnicode_FromWideChar(config->program_name, -1); ++ program_name = PyUnicode_FromWideChar(config->program_name, -1); + if (program_name == NULL) { + Py_DECREF(filename); + PyErr_Print(); + return -1; + } + +- int res = pymain_run_file_obj(program_name, filename, +- config->skip_source_first_line); ++ res = pymain_run_file_obj(program_name, filename, ++ config->skip_source_first_line); + Py_DECREF(filename); + Py_DECREF(program_name); + return res; +@@ -388,12 +413,20 @@ static int + pymain_run_startup(PyConfig *config, int *exitcode) + { + int ret; ++ PyObject *startup = NULL; ++#ifdef MS_WINDOWS ++ const wchar_t *env; ++#else ++ const char *env; ++#endif ++ FILE *fp; ++ PyCompilerFlags cf; ++ + if (!config->use_environment) { + return 0; + } +- PyObject *startup = NULL; + #ifdef MS_WINDOWS +- const wchar_t *env = _wgetenv(L"PYTHONSTARTUP"); ++ env = _wgetenv(L"PYTHONSTARTUP"); + if (env == NULL || env[0] == L'\0') { + return 0; + } +@@ -402,7 +435,7 @@ pymain_run_startup(PyConfig *config, int *exitcode) + goto error; + } + #else +- const char *env = _Py_GetEnv(config->use_environment, "PYTHONSTARTUP"); ++ env = _Py_GetEnv(config->use_environment, "PYTHONSTARTUP"); + if (env == NULL) { + return 0; + } +@@ -415,7 +448,7 @@ pymain_run_startup(PyConfig *config, int *exitcode) + goto error; + } + +- FILE *fp = _Py_fopen_obj(startup, "r"); ++ fp = _Py_fopen_obj(startup, "r"); + if (fp == NULL) { + int save_errno = errno; + PyErr_Clear(); +@@ -426,7 +459,10 @@ pymain_run_startup(PyConfig *config, int *exitcode) + goto error; + } + +- PyCompilerFlags cf = _PyCompilerFlags_INIT; ++ /* See pymain_run_command() above for why this is not ++ _PyCompilerFlags_INIT (VC6 has no compound literals). */ ++ cf.cf_flags = 0; ++ cf.cf_feature_version = PY_MINOR_VERSION; + (void) _PyRun_SimpleFileObject(fp, startup, 0, &cf); + PyErr_Clear(); + fclose(fp); +@@ -482,11 +518,15 @@ pymain_run_interactive_hook(int *exitcode) + static int + pymain_run_stdin(PyConfig *config) + { ++ PyCompilerFlags cf; ++ int run; ++ + if (stdin_is_interactive(config)) { ++ int exitcode; ++ + config->inspect = 0; + Py_InspectFlag = 0; /* do exit on SystemExit */ + +- int exitcode; + if (pymain_run_startup(config, &exitcode)) { + return exitcode; + } +@@ -505,8 +545,11 @@ pymain_run_stdin(PyConfig *config) + return pymain_exit_err_print(); + } + +- PyCompilerFlags cf = _PyCompilerFlags_INIT; +- int run = PyRun_AnyFileExFlags(stdin, "", 0, &cf); ++ /* See pymain_run_command() above for why this is not ++ _PyCompilerFlags_INIT (VC6 has no compound literals). */ ++ cf.cf_flags = 0; ++ cf.cf_feature_version = PY_MINOR_VERSION; ++ run = PyRun_AnyFileExFlags(stdin, "", 0, &cf); + return (run != 0); + } + +@@ -514,6 +557,9 @@ pymain_run_stdin(PyConfig *config) + static void + pymain_repl(PyConfig *config, int *exitcode) + { ++ PyCompilerFlags cf; ++ int res; ++ + /* Check this environment variable at the end, to give programs the + opportunity to set it from Python. */ + if (!config->inspect && _Py_GetEnv(config->use_environment, "PYTHONINSPECT")) { +@@ -535,8 +581,11 @@ pymain_repl(PyConfig *config, int *exitcode) + return; + } + +- PyCompilerFlags cf = _PyCompilerFlags_INIT; +- int res = PyRun_AnyFileFlags(stdin, "", &cf); ++ /* See pymain_run_command() above for why this is not ++ _PyCompilerFlags_INIT (VC6 has no compound literals). */ ++ cf.cf_flags = 0; ++ cf.cf_feature_version = PY_MINOR_VERSION; ++ res = PyRun_AnyFileFlags(stdin, "", &cf); + *exitcode = (res != 0); + } + +@@ -718,11 +767,15 @@ pymain_main(_PyArgv *args) + int + Py_Main(int argc, wchar_t **argv) + { +- _PyArgv args = { +- .argc = argc, +- .use_bytes_argv = 0, +- .bytes_argv = NULL, +- .wchar_argv = argv}; ++ /* VC6 has no C99 designated initializers, so the aggregate initializer ++ below cannot be used; assign each field explicitly instead (field ++ order does not matter for plain assignment, unlike the initializer ++ list it replaces). */ ++ _PyArgv args; ++ args.argc = argc; ++ args.use_bytes_argv = 0; ++ args.bytes_argv = NULL; ++ args.wchar_argv = argv; + return pymain_main(&args); + } + +@@ -730,11 +783,12 @@ Py_Main(int argc, wchar_t **argv) + int + Py_BytesMain(int argc, char **argv) + { +- _PyArgv args = { +- .argc = argc, +- .use_bytes_argv = 1, +- .bytes_argv = argv, +- .wchar_argv = NULL}; ++ /* See Py_Main() above: VC6 has no C99 designated initializers. */ ++ _PyArgv args; ++ args.argc = argc; ++ args.use_bytes_argv = 1; ++ args.bytes_argv = argv; ++ args.wchar_argv = NULL; + return pymain_main(&args); + } + +diff --git a/PC/pyconfig.h b/PC/pyconfig.h +index 959b851..6d8ce84 100644 +--- a/PC/pyconfig.h ++++ b/PC/pyconfig.h +@@ -256,6 +256,19 @@ typedef int pid_t; + # define PY_ULLONG_MAX _UI64_MAX + #endif + ++/* Spelling of 64 bit integer literal suffixes. MSVC before Visual C++ .NET ++ (_MSC_VER < 1300, i.e. Visual C++ 6.0 and earlier) rejects the C99 LL/ULL ++ suffixes and spells them i64/ui64 instead. */ ++#ifndef PY_LL ++# if defined(_MSC_VER) && _MSC_VER < 1300 ++# define PY_LL(x) x##i64 ++# define PY_ULL(x) x##ui64 ++# else ++# define PY_LL(x) x##LL ++# define PY_ULL(x) x##ULL ++# endif ++#endif ++ + /* For Windows the Python core is in a DLL by default. Test + Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ + #if !defined(MS_NO_COREDLL) && !defined(Py_NO_ENABLE_SHARED) +diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c +index 9248e97..a2220e9 100644 +--- a/Python/pylifecycle.c ++++ b/Python/pylifecycle.c +@@ -88,8 +88,14 @@ _Py_COMP_DIAG_IGNORE_DEPR_DECLS + + #if defined(MS_WINDOWS) + ++# if defined(_MSC_VER) && _MSC_VER < 1300 ++/* VC6 does not support #pragma section plus __declspec(allocate()). ++ Use its older data_seg spelling for this initialized object instead. */ ++# pragma data_seg("PyRuntime") ++# else + #pragma section("PyRuntime", read, write) + __declspec(allocate("PyRuntime")) ++# endif + + #elif defined(__APPLE__) + +@@ -104,6 +110,9 @@ _PyRuntimeState _PyRuntime + __attribute__ ((section (".PyRuntime"))) + #endif + = _PyRuntimeState_INIT; ++#if defined(MS_WINDOWS) && defined(_MSC_VER) && _MSC_VER < 1300 ++# pragma data_seg() ++#endif + _Py_COMP_DIAG_POP + + static int runtime_initialized = 0; diff --git a/compat/msvc600/stdbool.h b/compat/msvc600/stdbool.h new file mode 100644 index 0000000..7a7d15c --- /dev/null +++ b/compat/msvc600/stdbool.h @@ -0,0 +1,35 @@ +#ifndef PYTHON_WIN9X_MSVC600_STDBOOL_H +#define PYTHON_WIN9X_MSVC600_STDBOOL_H + +/* VC6-only fallback for the C99 header. + * + * Visual C++ 6.0 (_MSC_VER 1200) predates C99 and ships no ; + * in VC6's C mode `bool`/`true`/`false` are not keywords at all (they are + * C++-only there). This project-local header is found ahead of any system + * only because compat/msvc600 is placed first on the VC6 probe + * scripts' include path (scripts/vc6-probe.sh, scripts/vc6-probe-core.sh); + * it is never installed anywhere a standards-conforming compiler's own + * would be found instead, so it never shadows a real one. The + * _MSC_VER guard below is defence in depth for that same reason, and keeps + * this file inert (and diagnosable, via the #error) if it is ever reached + * by a compiler other than the VC6 this port targets. + * + * Modeled on the standard C99 contract: defines the macros + * bool, true, false, and __bool_true_false_are_defined, and (matching + * C99, which requires a program be able to #undef/redefine bool/true/ + * false) leaves them as plain macros rather than a typedef, so existing + * C99-style `#undef bool` idioms, were CPython to use one, would still + * work. + */ + +#if !defined(_MSC_VER) || _MSC_VER >= 1300 +#error "compat/msvc600/stdbool.h is a VC6-only (_MSC_VER < 1300) fallback; a standard should be used instead." +#endif + +#define bool int +#define true 1 +#define false 0 + +#define __bool_true_false_are_defined 1 + +#endif /* PYTHON_WIN9X_MSVC600_STDBOOL_H */ diff --git a/scripts/fixtures/vc6_atomic_smoke.c b/scripts/fixtures/vc6_atomic_smoke.c new file mode 100644 index 0000000..3ca86e6 --- /dev/null +++ b/scripts/fixtures/vc6_atomic_smoke.c @@ -0,0 +1,36 @@ +/* Focused, isolated compile smoke test for Include/internal/pycore_atomic.h + * under VC6/_M_IX86. + * + * Deliberately does NOT go through Python.h or any other pycore_*.h header: + * pycore_interp.h (reached transitively from a real core translation unit + * like Modules/main.c) additionally requires , a separate, + * unrelated VC6 gap tracked in PORT_STATUS.md. Isolating pycore_atomic.h + * here keeps this probe's result attributable to the atomics compatibility + * work specifically. + * + * Py_BUILD_CORE is supplied by the probe script (/D Py_BUILD_CORE), the + * same way it is for a real core translation unit. (resolved + * to compat/msvc600/inttypes.h for VC6 via the include path) provides + * uintptr_t, which pycore_atomic.h needs but does not itself include; in + * a real build this comes from Python.h/pyport.h, included ahead of any + * pycore_*.h header. + */ +#include +#include "pycore_atomic.h" + +static _Py_atomic_int atomic_int_value; +static _Py_atomic_address atomic_addr_value; + +int +vc6_atomic_smoke(void) +{ + _Py_atomic_store(&atomic_int_value, 1); + _Py_atomic_store_relaxed(&atomic_int_value, 2); + _Py_atomic_store(&atomic_addr_value, (uintptr_t)0); + _Py_atomic_store_relaxed(&atomic_addr_value, (uintptr_t)1); + + return _Py_atomic_load(&atomic_int_value) + + _Py_atomic_load_relaxed(&atomic_int_value) + + (int)_Py_atomic_load(&atomic_addr_value) + + (int)_Py_atomic_load_relaxed(&atomic_addr_value); +} diff --git a/scripts/fixtures/vc6_compound_literal_smoke.c b/scripts/fixtures/vc6_compound_literal_smoke.c new file mode 100644 index 0000000..73709db --- /dev/null +++ b/scripts/fixtures/vc6_compound_literal_smoke.c @@ -0,0 +1,64 @@ +/* Focused compile smoke test for the VC6 C99 compound-literal / + * designated-initializer fixes applied to cpython/Modules/main.c. + * + * VC6 (a pre-C99 compiler) supports neither C99 compound literals + * (`(PyStatus){...}`) nor C99 designated initializers (`{ .field = value, + * ... }`), at any position in a block. Modules/main.c relied on both via + * three distinct patterns: + * + * 1. `status = _PyStatus_OK();` -- a compound-literal macro used as a + * plain assignment's right-hand side. Fixed by calling the existing + * public API function PyStatus_Ok() instead, which + * Python/initconfig.c defines as `return _PyStatus_OK();` -- the + * identical value, no header change needed. + * 2. `cf = _PyCompilerFlags_INIT;` -- likewise a compound-literal macro + * used as a plain assignment. Fixed by assigning PyCompilerFlags's + * two fields explicitly (cf_flags, cf_feature_version), matching the + * macro's own definition field-for-field. + * 3. `_PyArgv args = { .argc = argc, ... };` -- a designated-initializer + * aggregate initializer with no macro involved. Fixed by declaring + * the variable bare and assigning each field explicitly; field order + * does not matter for plain assignment, unlike an initializer list. + * + * See PORT_STATUS.md ("Compound-literal/designated-initializer slice") for + * full detail. Like vc6_mixed_decls_smoke.c, this fixture includes the + * real "Python.h" (Py_BUILD_CORE is supplied by the probe script) because + * PyStatus, PyCompilerFlags, and _PyArgv are real CPython types pulled in + * through the normal public/internal header chain, the same way + * Modules/main.c itself reaches them. + */ +#include "Python.h" +#include "pycore_initconfig.h" // _PyArgv, _PyStatus_OK() + +PyStatus +vc6_status_ok_smoke(void) +{ + PyStatus status; + + /* pattern 1: was `status = _PyStatus_OK();` */ + status = PyStatus_Ok(); + return status; +} + +PyCompilerFlags +vc6_compiler_flags_smoke(void) +{ + PyCompilerFlags cf; + + /* pattern 2: was `cf = _PyCompilerFlags_INIT;` */ + cf.cf_flags = 0; + cf.cf_feature_version = PY_MINOR_VERSION; + return cf; +} + +Py_ssize_t +vc6_argv_designated_init_smoke(int argc, wchar_t **argv) +{ + /* pattern 3: was `_PyArgv args = { .argc = argc, ... };` */ + _PyArgv args; + args.argc = argc; + args.use_bytes_argv = 0; + args.bytes_argv = NULL; + args.wchar_argv = argv; + return args.argc; +} diff --git a/scripts/fixtures/vc6_decl_order_smoke.c b/scripts/fixtures/vc6_decl_order_smoke.c new file mode 100644 index 0000000..f7c65df --- /dev/null +++ b/scripts/fixtures/vc6_decl_order_smoke.c @@ -0,0 +1,162 @@ +/* Focused compile smoke test for the VC6 C89-mixed-declarations fixes + * applied to cpython/Modules/main.c. + * + * VC6 is a C89-only compiler (plus Microsoft extensions): block-scope + * declarations must precede all statements in that block. Modules/main.c + * declared many locals mid-function (typically right before first use, + * C99 style); the fix hoists each declaration to the top of its enclosing + * block, without initializer, and turns the original `TYPE name = expr;` + * into a plain `name = expr;` assignment at its original position. This + * changes no evaluation order and no semantics. See PORT_STATUS.md + * ("Modules/main.c mixed-declarations slice") for full detail. + * + * This fixture is deliberately self-contained (no CPython headers) so it + * exercises the *shape* of every hoist pattern used in main.c, independent + * of main.c's own evolution: + * + * 1. a bare declaration hoisted above an early-return statement + * (pymain_init's `preconfig`/`config`); + * 2. a declaration whose initializer is a side-effecting call, hoisted + * bare and assigned at its original site so the call itself still + * happens after the same preceding statements + * (pymain_import_readline's `mod`, pymain_run_file_obj's `fp`); + * 3. two declarations in a row where hoisting the first turns its site + * into an assignment statement, which would otherwise make the + * second declaration (immediately following) a new violation too + * (pymain_run_file_obj's `cf`/`run`); + * 4. a declaration inside a nested block (its own scope), after + * statements already executed in that block (pymain_run_stdin's + * nested `exitcode`); + * 5. a platform-conditional declaration with the same name but a + * different type per branch, hoisted under the same #ifdef structure + * at the top of the function (pymain_run_startup's `env`). + */ + +#define TARGET_WINDOWS 1 + +/* --- pattern 1: bare declaration hoisted above an early return --- */ +static int +decl_after_early_return(int flag) +{ + int status; + int extra; + + status = flag ? 0 : -1; + if (status != 0) { + return status; + } + + extra = 7; + return extra; +} + +/* --- pattern 2: side-effecting-initializer declaration hoisted bare, + * assigned at its original position so the call still happens after the + * same preceding statements --- */ +static int +side_effect(int *counter) +{ + (*counter)++; + return *counter; +} + +static int +decl_with_side_effecting_init(int *counter) +{ + int guard; + int value; + + guard = (*counter < 0); + if (guard) { + return -1; + } + + value = side_effect(counter); + return value; +} + +/* --- pattern 3: two declarations in a row, first one hoisted becomes an + * assignment, so the second (immediately following) must hoist too --- */ +static int +decl_pair_after_hoist(int *counter) +{ + int guard; + int a; + int b; + + guard = (*counter < 0); + if (guard) { + return -1; + } + + a = side_effect(counter); + b = side_effect(counter); + return a + b; +} + +/* --- pattern 4: nested-block declaration after statements already + * executed within that same nested block --- */ +static int +decl_after_statement_in_nested_block(int interactive) +{ + int outer_result = 0; + + if (interactive) { + int inner_value; + + outer_result += 1; + outer_result += 2; + + inner_value = outer_result * 10; + return inner_value; + } + + return outer_result; +} + +/* --- pattern 5: platform-conditional declaration, same name, different + * type per branch, hoisted under the same #ifdef at the top --- */ +static long +decl_platform_conditional(void) +{ + int ret; +#ifdef TARGET_WINDOWS + const char *env; +#else + const long *env; +#endif + + ret = 1; + if (!ret) { + return 0; + } + +#ifdef TARGET_WINDOWS + env = "PYTHONSTARTUP"; + if (env == 0) { + return 0; + } + return (long)env[0]; +#else + env = 0; + if (env == 0) { + return 0; + } + return *env; +#endif +} + +int +main(void) +{ + int counter = 0; + int total = 0; + + total += decl_after_early_return(1); + total += decl_with_side_effecting_init(&counter); + total += decl_pair_after_hoist(&counter); + total += decl_after_statement_in_nested_block(1); + total += (int)decl_platform_conditional(); + + return (total >= 0) ? 0 : 1; +} diff --git a/scripts/fixtures/vc6_mixed_decls_smoke.c b/scripts/fixtures/vc6_mixed_decls_smoke.c new file mode 100644 index 0000000..269e349 --- /dev/null +++ b/scripts/fixtures/vc6_mixed_decls_smoke.c @@ -0,0 +1,54 @@ +/* Focused compile smoke test for the VC6 C89-mixed-declarations fixes in + * four Include/internal/pycore_*.h headers: pycore_code.h, pycore_dict.h, + * pycore_list.h, and pycore_call.h. + * + * VC6 is a C89-only compiler (plus Microsoft extensions): block-scope + * declarations must precede all statements in that block. CPython 3.11's + * source freely uses C99-style declarations after statements (e.g. after + * an assert()); the fix hoists each such declaration to the top of its + * enclosing block without changing evaluation order or behavior. See + * PORT_STATUS.md ("Mixed-declarations header slice") for full detail. + * + * Unlike scripts/fixtures/vc6_atomic_smoke.c and vc6_stdbool_smoke.c, this + * fixture includes the real "Python.h" (Py_BUILD_CORE is supplied by the + * probe script) rather than isolating the pycore_*.h headers from it: + * these four headers assume types (PyObject, PyCodeObject, PyListObject, + * PyDictValues, vectorcallfunc, ...) that come from the normal public + * header chain, the same way real CPython .c sources reach them. + */ +#include "Python.h" +#include "pycore_code.h" +#include "pycore_dict.h" +#include "pycore_list.h" +#include "pycore_call.h" + +int +vc6_locals_kind_smoke(PyObject *kinds, int i, _PyLocals_Kind kind) +{ + _PyLocals_SetKind(kinds, i, kind); + return (int)_PyLocals_GetKind(kinds, i); +} + +uint16_t +vc6_adaptive_counter_smoke(uint16_t counter) +{ + return adaptive_counter_backoff(counter); +} + +void +vc6_dict_values_smoke(PyDictValues *values, Py_ssize_t ix) +{ + _PyDictValues_AddToInsertionOrder(values, ix); +} + +int +vc6_list_append_smoke(PyListObject *self, PyObject *newitem) +{ + return _PyList_AppendTakeRef(self, newitem); +} + +vectorcallfunc +vc6_vectorcall_smoke(PyObject *callable) +{ + return _PyVectorcall_FunctionInline(callable); +} diff --git a/scripts/fixtures/vc6_stdbool_smoke.c b/scripts/fixtures/vc6_stdbool_smoke.c new file mode 100644 index 0000000..47710a0 --- /dev/null +++ b/scripts/fixtures/vc6_stdbool_smoke.c @@ -0,0 +1,31 @@ +/* Focused, isolated compile smoke test for compat/msvc600/stdbool.h under + * VC6. + * + * Deliberately does NOT go through pycore_interp.h or any other pycore_*.h + * header: this isolates the result to the shim itself, the + * same way scripts/fixtures/vc6_atomic_smoke.c isolates pycore_atomic.h. + * Modules/main.c (see scripts/vc6-probe-core.test.sh) is the real-header + * reference case that proves compat/msvc600/stdbool.h is actually what + * pycore_interp.h's `#include ` resolves to via the VC6 probe + * scripts' include path. + */ +#include + +static bool vc6_stdbool_flag = true; + +int +vc6_stdbool_smoke(void) +{ + bool a = true; + bool b = false; + bool c = a && !b; + + vc6_stdbool_flag = c; + + if (!__bool_true_false_are_defined) { + return -1; + } + + return (int)(a == true) + (int)(b == false) + (int)(c == true) + + (int)vc6_stdbool_flag; +} diff --git a/scripts/vc6-probe-core.sh b/scripts/vc6-probe-core.sh new file mode 100755 index 0000000..5af241c --- /dev/null +++ b/scripts/vc6-probe-core.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# +# Py_BUILD_CORE compile probe for the Windows 9x port. +# +# scripts/vc6-probe.sh only ever compiles a public-header translation unit +# (Programs/python.c). CPython's internal (`Include/internal/pycore_*.h`) +# headers are a different, stricter surface: every one of them requires +# Py_BUILD_CORE to be defined and Include/internal on the include path, and +# they use compiler features the public headers avoid. This script probes +# that surface directly by compiling Modules/main.c, the first internal +# translation unit CPython's own build reaches, the same way +# scripts/vc6-probe.sh probes the public one: run the external MSVC600 +# `cl.exe` under Wine against the CPython 3.11.16 baseline in ./cpython, +# using the project-local compatibility headers in ./compat/msvc600. +# Compile-only (/c): this measures how far the VC6 front end gets, it does +# not attempt to link. +# +# The MSVC600 bundle is an external, local-only prerequisite; nothing under +# MSVC600/ is part of this project's source. +# +# Usage: +# scripts/vc6-probe-core.sh # probe Modules/main.c +# scripts/vc6-probe-core.sh path/to/other.c # probe another core TU +# +# Environment: +# MSVC600_ROOT override the MSVC600 location (default: /MSVC600) +# WINEPREFIX override the Wine prefix (default: ~/.wine-win9xport) + +set -u + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Reuse winpath()/strip_prefix() from vc6-probe.sh instead of duplicating +# them; sourcing it does not run the probe itself (see its own guard). +# shellcheck source=scripts/vc6-probe.sh +source "$REPO_ROOT/scripts/vc6-probe.sh" + +run_probe_core() { + MSVC600_ROOT="${MSVC600_ROOT:-$REPO_ROOT/MSVC600}" + BUILD_DIR="$REPO_ROOT/build/vc6-probe-core" + + SOURCE_REL="${1:-cpython/Modules/main.c}" + SOURCE_ABS="$REPO_ROOT/$SOURCE_REL" + + CL="$MSVC600_ROOT/VC98/Bin/CL.EXE" + VCVARS="$MSVC600_ROOT/VC98/Bin/VCVARS32.BAT" + + for required in "$CL" "$VCVARS" "$MSVC600_ROOT/VC98/Include" \ + "$MSVC600_ROOT/Common/MSDev98/Bin" "$SOURCE_ABS"; do + if [ ! -e "$required" ]; then + echo "vc6-probe-core: missing prerequisite: $required" >&2 + return 2 + fi + done + + if ! command -v wine >/dev/null 2>&1; then + echo "vc6-probe-core: wine is not installed" >&2 + return 2 + fi + + export WINEPREFIX="${WINEPREFIX:-$HOME/.wine-win9xport}" + export WINEDEBUG="${WINEDEBUG:--all}" + + # CL.EXE loads C1/C2 from VC98/Bin and MSPDB60.DLL from Common/MSDev98/Bin. + export WINEPATH="$(winpath "$MSVC600_ROOT/VC98/Bin");$(winpath "$MSVC600_ROOT/Common/MSDev98/Bin")" + + mkdir -p "$BUILD_DIR" + + # Include/internal must come before Include so pycore_*.h headers can + # find each other with their own bare #include "pycore_foo.h" style, + # same order CPython's own PC build uses. + INCLUDE_ARGS=( + "$(winpath "$REPO_ROOT/compat/msvc600")" + "$(winpath "$REPO_ROOT/cpython/Include/internal")" + "$(winpath "$REPO_ROOT/cpython/Include")" + "$(winpath "$REPO_ROOT/cpython/PC")" + "$(winpath "$MSVC600_ROOT/VC98/Include")" + ) + + echo "== MSVC600 Py_BUILD_CORE compile probe ==" + echo "source : $SOURCE_REL" + echo "prefix : $WINEPREFIX" + echo + + CMD="call $(winpath "$VCVARS") && cd /d $(winpath "$REPO_ROOT/cpython") && cl /nologo /D Py_BUILD_CORE" + for include_arg in "${INCLUDE_ARGS[@]}"; do + CMD+=" /I $include_arg" + done + CMD+=" /c $(winpath "$SOURCE_ABS") /Fo$(winpath "$BUILD_DIR")\\" + + wine cmd /c "$CMD" 2>&1 | + grep -v '^[0-9a-f]\{4\}:err:' | + strip_prefix "$(winpath "$REPO_ROOT")\\" + + status="${PIPESTATUS[0]}" + echo + echo "== cl.exe exit status: $status ==" + return "$status" +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + run_probe_core "$@" + exit "$?" +fi diff --git a/scripts/vc6-probe-core.test.sh b/scripts/vc6-probe-core.test.sh new file mode 100755 index 0000000..4bf4ef1 --- /dev/null +++ b/scripts/vc6-probe-core.test.sh @@ -0,0 +1,337 @@ +#!/usr/bin/env bash +# +# Regression tests for scripts/vc6-probe-core.sh and, through it, the VC6 +# _Py_ATOMIC_VC6_X86 branch added to +# cpython/Include/internal/pycore_atomic.h. +# +# Skipped (not failed) when wine + MSVC600 are unavailable, matching +# scripts/vc6-probe.test.sh's convention, since CI/dev boxes without the +# external MSVC600 bundle cannot exercise this. +# +# Usage: scripts/vc6-probe-core.test.sh + +set -u + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PROBE="$REPO_ROOT/scripts/vc6-probe-core.sh" + +fail=0 +pass() { echo "ok - $1"; } +bad() { echo "not ok - $1"; fail=1; } + +MSVC600_ROOT="${MSVC600_ROOT:-$REPO_ROOT/MSVC600}" +if command -v wine >/dev/null 2>&1 && [ -e "$MSVC600_ROOT/VC98/Bin/CL.EXE" ]; then + + # --- positive case: the isolated atomics smoke fixture compiles clean --- + out="$("$PROBE" scripts/fixtures/vc6_atomic_smoke.c 2>&1)" + status=$? + + if [ "$status" -eq 0 ]; then + pass "pycore_atomic.h VC6 branch: atomics smoke fixture compiles (exit 0)" + else + bad "pycore_atomic.h VC6 branch: atomics smoke fixture compiles (exit $status)" + echo "$out" + fi + + if printf '%s' "$out" | grep -qi 'error'; then + bad "atomics smoke fixture: no compiler errors in output" + else + pass "atomics smoke fixture: no compiler errors in output" + fi + + if printf '%s' "$out" | grep -q 'intrin.h'; then + bad "atomics smoke fixture: does not hit the intrin.h blocker" + else + pass "atomics smoke fixture: does not hit the intrin.h blocker" + fi + + # --- negative case: a deliberately invalid atomics usage still fails --- + BAD_SRC="$REPO_ROOT/scripts/fixtures/_vc6_atomic_smoke_bad.c" + cat > "$BAD_SRC" <<'EOF' +#include +#include "pycore_atomic.h" +int bad(void) { return this_identifier_does_not_exist; } +EOF + trap 'rm -f "$BAD_SRC"' EXIT + + out_bad="$("$PROBE" scripts/fixtures/_vc6_atomic_smoke_bad.c 2>&1)" + status_bad=$? + + if [ "$status_bad" -ne 0 ]; then + pass "probe-core reports nonzero exit for a broken translation unit" + else + bad "probe-core reports nonzero exit for a broken translation unit (got: $status_bad)" + fi + + if printf '%s' "$out_bad" | grep -q 'error C'; then + pass "probe-core output surfaces the compiler diagnostic" + else + bad "probe-core output surfaces the compiler diagnostic" + fi + + # --- reference case: Modules/main.c reaches past pycore_atomic.h --- + # main.c is expected to fail today (pycore_interp.h needs , + # a separate, unrelated VC6 gap) but must not fail *at* + # pycore_atomic.h/intrin.h; that would mean the atomics branch + # regressed even though the isolated fixture above passed. + out_main="$("$PROBE" cpython/Modules/main.c 2>&1)" + if printf '%s' "$out_main" | grep -q 'pycore_atomic.h.*intrin.h\|intrin.h.*No such file'; then + bad "Modules/main.c probe: does not regress to the pycore_atomic.h/intrin.h blocker" + else + pass "Modules/main.c probe: does not regress to the pycore_atomic.h/intrin.h blocker" + fi + + # --- positive case: the isolated stdbool smoke fixture compiles clean --- + out_bool="$("$PROBE" scripts/fixtures/vc6_stdbool_smoke.c 2>&1)" + status_bool=$? + + if [ "$status_bool" -eq 0 ]; then + pass "compat/msvc600/stdbool.h: stdbool smoke fixture compiles (exit 0)" + else + bad "compat/msvc600/stdbool.h: stdbool smoke fixture compiles (exit $status_bool)" + echo "$out_bool" + fi + + if printf '%s' "$out_bool" | grep -qi 'error'; then + bad "stdbool smoke fixture: no compiler errors in output" + else + pass "stdbool smoke fixture: no compiler errors in output" + fi + + # --- negative case: a deliberately invalid bool usage still fails --- + BAD_BOOL_SRC="$REPO_ROOT/scripts/fixtures/_vc6_stdbool_smoke_bad.c" + cat > "$BAD_BOOL_SRC" <<'EOF' +#include +int bad(void) { return this_identifier_does_not_exist; } +EOF + trap 'rm -f "$BAD_SRC" "$BAD_BOOL_SRC"' EXIT + + out_bool_bad="$("$PROBE" scripts/fixtures/_vc6_stdbool_smoke_bad.c 2>&1)" + status_bool_bad=$? + + if [ "$status_bool_bad" -ne 0 ]; then + pass "probe-core reports nonzero exit for a broken stdbool translation unit" + else + bad "probe-core reports nonzero exit for a broken stdbool translation unit (got: $status_bool_bad)" + fi + + if printf '%s' "$out_bool_bad" | grep -q 'error C'; then + pass "probe-core output surfaces the compiler diagnostic (stdbool case)" + else + bad "probe-core output surfaces the compiler diagnostic (stdbool case)" + fi + + # --- reference case: Modules/main.c reaches past pycore_interp.h's + # include. It is expected to still fail today -- the next, + # unrelated blocker is VC6's C89-only parser rejecting C99-style mixed + # declarations-and-code in several pycore_*.h inline functions (e.g. + # Include/internal/pycore_code.h's _PyLocals_GetKind(), which declares + # `char *ptr` after a preceding `assert(...)` statement) -- but it must + # not fail *at* anymore; that would mean this slice + # regressed even though the isolated fixture above passed. + if printf '%s' "$out_main" | grep -q "Cannot open include file: 'stdbool.h'"; then + bad "Modules/main.c probe: does not regress to the pycore_interp.h/stdbool.h blocker" + else + pass "Modules/main.c probe: does not regress to the pycore_interp.h/stdbool.h blocker" + fi + + # --- positive case: the isolated mixed-declarations smoke fixture + # compiles clean (pycore_code.h, pycore_dict.h, pycore_list.h, + # pycore_call.h all hoisted their C99 mid-block declarations) --- + out_decls="$("$PROBE" scripts/fixtures/vc6_mixed_decls_smoke.c 2>&1)" + status_decls=$? + + if [ "$status_decls" -eq 0 ]; then + pass "mixed-declarations header slice: smoke fixture compiles (exit 0)" + else + bad "mixed-declarations header slice: smoke fixture compiles (exit $status_decls)" + echo "$out_decls" + fi + + if printf '%s' "$out_decls" | grep -qi 'error'; then + bad "mixed-declarations smoke fixture: no compiler errors in output" + else + pass "mixed-declarations smoke fixture: no compiler errors in output" + fi + + # --- negative case: a deliberately invalid mixed-declarations usage + # still fails --- + BAD_DECLS_SRC="$REPO_ROOT/scripts/fixtures/_vc6_mixed_decls_smoke_bad.c" + cat > "$BAD_DECLS_SRC" <<'EOF' +#include "Python.h" +#include "pycore_code.h" +int bad(void) { return this_identifier_does_not_exist; } +EOF + trap 'rm -f "$BAD_SRC" "$BAD_BOOL_SRC" "$BAD_DECLS_SRC"' EXIT + + out_decls_bad="$("$PROBE" scripts/fixtures/_vc6_mixed_decls_smoke_bad.c 2>&1)" + status_decls_bad=$? + + if [ "$status_decls_bad" -ne 0 ]; then + pass "probe-core reports nonzero exit for a broken mixed-declarations translation unit" + else + bad "probe-core reports nonzero exit for a broken mixed-declarations translation unit (got: $status_decls_bad)" + fi + + if printf '%s' "$out_decls_bad" | grep -q 'error C'; then + pass "probe-core output surfaces the compiler diagnostic (mixed-declarations case)" + else + bad "probe-core output surfaces the compiler diagnostic (mixed-declarations case)" + fi + + # --- reference case: Modules/main.c no longer fails inside any of the + # four fixed pycore_*.h headers. It is expected to still fail today -- + # Modules/main.c itself uses the same C99 mixed-declarations pattern + # pervasively in its own function bodies, a much larger, whole-file + # source rewrite that is out of scope for this header-only slice; see + # PORT_STATUS.md's "Mixed-declarations header slice" section for the + # documented boundary -- but it must not fail *inside* + # pycore_code.h/pycore_dict.h/pycore_list.h/pycore_call.h anymore; + # that would mean this slice regressed even though the isolated + # fixture above passed. + if printf '%s' "$out_main" | grep -qE 'pycore_(code|dict|list|call)\.h\([0-9]+\)'; then + bad "Modules/main.c probe: does not regress to the pycore_code/dict/list/call.h mixed-declarations blocker" + else + pass "Modules/main.c probe: does not regress to the pycore_code/dict/list/call.h mixed-declarations blocker" + fi + + # --- positive case: the isolated declaration-order smoke fixture + # compiles clean. It exercises, self-contained, the same five hoist + # shapes applied to Modules/main.c itself (bare hoist above an early + # return, side-effecting-initializer hoist, a hoisted pair where fixing + # the first declaration would otherwise turn the second into a new + # violation, a nested-block hoist, and a platform-conditional same-name + # hoist) so this guards the general transformation, independent of + # main.c's own future edits. --- + out_decl_order="$("$PROBE" scripts/fixtures/vc6_decl_order_smoke.c 2>&1)" + status_decl_order=$? + + if [ "$status_decl_order" -eq 0 ]; then + pass "Modules/main.c declaration-order slice: smoke fixture compiles (exit 0)" + else + bad "Modules/main.c declaration-order slice: smoke fixture compiles (exit $status_decl_order)" + echo "$out_decl_order" + fi + + if printf '%s' "$out_decl_order" | grep -qi 'error'; then + bad "declaration-order smoke fixture: no compiler errors in output" + else + pass "declaration-order smoke fixture: no compiler errors in output" + fi + + # --- negative case: a deliberately invalid declaration-order usage + # still fails --- + BAD_DECL_ORDER_SRC="$REPO_ROOT/scripts/fixtures/_vc6_decl_order_smoke_bad.c" + cat > "$BAD_DECL_ORDER_SRC" <<'EOF' +int bad(void) { return this_identifier_does_not_exist; } +EOF + trap 'rm -f "$BAD_SRC" "$BAD_BOOL_SRC" "$BAD_DECLS_SRC" "$BAD_DECL_ORDER_SRC"' EXIT + + out_decl_order_bad="$("$PROBE" scripts/fixtures/_vc6_decl_order_smoke_bad.c 2>&1)" + status_decl_order_bad=$? + + if [ "$status_decl_order_bad" -ne 0 ]; then + pass "probe-core reports nonzero exit for a broken declaration-order translation unit" + else + bad "probe-core reports nonzero exit for a broken declaration-order translation unit (got: $status_decl_order_bad)" + fi + + if printf '%s' "$out_decl_order_bad" | grep -q 'error C'; then + pass "probe-core output surfaces the compiler diagnostic (declaration-order case)" + else + bad "probe-core output surfaces the compiler diagnostic (declaration-order case)" + fi + + # --- reference case: Modules/main.c no longer fails on any C89 + # mixed-declaration site anywhere in its own body (the C2146/C2065/ + # C2275 "illegal use of this type as an expression" pattern tied to a + # declaration keyword). At the time this check was written, main.c + # still failed with a distinct, out-of-scope blocker instead (C99 + # compound literals/designated initializers, see below); that blocker + # is now also fixed (see the compound-literal checks further down), + # but this check's job is narrower and unchanged: main.c must not show + # the declaration-order-specific error codes; that would mean this + # slice regressed even though the isolated fixture above passed. + if printf '%s' "$out_main" | grep -qE "main\.c\([0-9]+\) : error C2146|main\.c\([0-9]+\) : error C2065.*undeclared identifier|main\.c\([0-9]+\) : error C2275"; then + bad "Modules/main.c probe: does not regress to a C89 mixed-declaration error inside main.c's own body" + else + pass "Modules/main.c probe: does not regress to a C89 mixed-declaration error inside main.c's own body" + fi + + # --- positive case: the isolated compound-literal/designated- + # initializer smoke fixture compiles clean. It exercises, self- + # contained (aside from the real Python.h/pycore_initconfig.h types + # it needs), the three replacement patterns applied to main.c: calling + # PyStatus_Ok() instead of the _PyStatus_OK() compound-literal macro, + # explicit field assignment instead of the _PyCompilerFlags_INIT + # compound-literal macro, and explicit field assignment instead of a + # _PyArgv designated-initializer aggregate. --- + out_compound="$("$PROBE" scripts/fixtures/vc6_compound_literal_smoke.c 2>&1)" + status_compound=$? + + if [ "$status_compound" -eq 0 ]; then + pass "compound-literal slice: smoke fixture compiles (exit 0)" + else + bad "compound-literal slice: smoke fixture compiles (exit $status_compound)" + echo "$out_compound" + fi + + if printf '%s' "$out_compound" | grep -qi 'error'; then + bad "compound-literal smoke fixture: no compiler errors in output" + else + pass "compound-literal smoke fixture: no compiler errors in output" + fi + + # --- negative case: a deliberately invalid compound-literal-fixture + # usage still fails --- + BAD_COMPOUND_SRC="$REPO_ROOT/scripts/fixtures/_vc6_compound_literal_smoke_bad.c" + cat > "$BAD_COMPOUND_SRC" <<'EOF' +#include "Python.h" +#include "pycore_initconfig.h" +int bad(void) { return this_identifier_does_not_exist; } +EOF + trap 'rm -f "$BAD_SRC" "$BAD_BOOL_SRC" "$BAD_DECLS_SRC" "$BAD_DECL_ORDER_SRC" "$BAD_COMPOUND_SRC"' EXIT + + out_compound_bad="$("$PROBE" scripts/fixtures/_vc6_compound_literal_smoke_bad.c 2>&1)" + status_compound_bad=$? + + if [ "$status_compound_bad" -ne 0 ]; then + pass "probe-core reports nonzero exit for a broken compound-literal translation unit" + else + bad "probe-core reports nonzero exit for a broken compound-literal translation unit (got: $status_compound_bad)" + fi + + if printf '%s' "$out_compound_bad" | grep -q 'error C'; then + pass "probe-core output surfaces the compiler diagnostic (compound-literal case)" + else + bad "probe-core output surfaces the compiler diagnostic (compound-literal case)" + fi + + # --- reference case: Modules/main.c itself no longer shows any C2059 + # "syntax error : '{'"/"syntax error : '.'" (the compound-literal/ + # designated-initializer error signature), and -- since this was the + # last recorded blocker for main.c as a compile-only (/c) translation + # unit -- now compiles with exit status 0. This is a compile-only + # result: no link or runtime success is claimed (vc6-probe-core.sh is + # /c only; see its header comment). --- + if printf '%s' "$out_main" | grep -q 'main\.c([0-9]*) : error C2059'; then + bad "Modules/main.c probe: does not regress to the compound-literal/designated-initializer C2059 blocker" + else + pass "Modules/main.c probe: does not regress to the compound-literal/designated-initializer C2059 blocker" + fi + + out_main_status="$("$PROBE" cpython/Modules/main.c >/dev/null 2>&1; echo $?)" + if [ "$out_main_status" -eq 0 ]; then + pass "Modules/main.c probe: compiles clean end-to-end (compile-only, exit 0)" + else + bad "Modules/main.c probe: compiles clean end-to-end (compile-only, got exit $out_main_status)" + fi +else + echo "skip - wine/MSVC600 not available; skipping live probe-core integration checks" +fi + +if [ "$fail" -ne 0 ]; then + echo "FAILED" + exit 1 +fi +echo "ALL TESTS PASSED" diff --git a/scripts/vc6-probe.sh b/scripts/vc6-probe.sh new file mode 100755 index 0000000..fdecf00 --- /dev/null +++ b/scripts/vc6-probe.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# +# Minimal reproducible MSVC 6.0 compile probe for the Windows 9x port. +# +# Runs the external MSVC600 `cl.exe` under Wine against the CPython 3.11.16 +# baseline in ./cpython, using the project-local compatibility headers in +# ./compat/msvc600. Compile-only (/c): this probe measures how far the VC6 +# front end gets through the public headers, it does not attempt to link. +# +# The MSVC600 bundle is an external, local-only prerequisite; nothing under +# MSVC600/ is part of this project's source. +# +# Usage: +# scripts/vc6-probe.sh # probe Programs/python.c +# scripts/vc6-probe.sh path/to/other.c # probe another translation unit +# +# Environment: +# MSVC600_ROOT override the MSVC600 location (default: /MSVC600) +# WINEPREFIX override the Wine prefix (default: ~/.wine-win9xport) + +set -u + +# Absolute POSIX path -> Wine drive Z: path. +winpath() { printf 'Z:%s' "${1//\//\\}"; } + +# Strip a literal (non-regex) prefix from every line of stdin. +# +# sed and bash glob patterns both treat backslashes in the *pattern* as +# escape characters, so passing a Windows path (full of literal `\` +# separators) as a sed/glob pattern silently fails to match: `\h`, `\u`, +# etc. are consumed as (mostly no-op) escapes instead of literal +# characters. Quoting the parameter inside a bash `${var//pattern/}` +# expansion forces literal (non-glob) matching, which sidesteps the +# problem entirely. +strip_prefix() { + local prefix="$1" line + while IFS= read -r line || [ -n "$line" ]; do + printf '%s\n' "${line//"$prefix"/}" + done +} + +run_probe() { + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + MSVC600_ROOT="${MSVC600_ROOT:-$REPO_ROOT/MSVC600}" + BUILD_DIR="$REPO_ROOT/build/vc6-probe" + + SOURCE_REL="${1:-cpython/Programs/python.c}" + SOURCE_ABS="$REPO_ROOT/$SOURCE_REL" + + CL="$MSVC600_ROOT/VC98/Bin/CL.EXE" + VCVARS="$MSVC600_ROOT/VC98/Bin/VCVARS32.BAT" + + for required in "$CL" "$VCVARS" "$MSVC600_ROOT/VC98/Include" \ + "$MSVC600_ROOT/Common/MSDev98/Bin" "$SOURCE_ABS"; do + if [ ! -e "$required" ]; then + echo "vc6-probe: missing prerequisite: $required" >&2 + return 2 + fi + done + + if ! command -v wine >/dev/null 2>&1; then + echo "vc6-probe: wine is not installed" >&2 + return 2 + fi + + export WINEPREFIX="${WINEPREFIX:-$HOME/.wine-win9xport}" + export WINEDEBUG="${WINEDEBUG:--all}" + + # CL.EXE loads C1/C2 from VC98/Bin and MSPDB60.DLL from Common/MSDev98/Bin. + export WINEPATH="$(winpath "$MSVC600_ROOT/VC98/Bin");$(winpath "$MSVC600_ROOT/Common/MSDev98/Bin")" + + mkdir -p "$BUILD_DIR" + + INCLUDE_ARGS=( + "$(winpath "$REPO_ROOT/compat/msvc600")" + "$(winpath "$REPO_ROOT/cpython/Include")" + "$(winpath "$REPO_ROOT/cpython/PC")" + "$(winpath "$MSVC600_ROOT/VC98/Include")" + ) + + echo "== MSVC600 compile probe ==" + echo "source : $SOURCE_REL" + echo "prefix : $WINEPREFIX" + echo + + CMD="call $(winpath "$VCVARS") && cd /d $(winpath "$REPO_ROOT/cpython") && cl /nologo" + for include_arg in "${INCLUDE_ARGS[@]}"; do + CMD+=" /I $include_arg" + done + CMD+=" /c $(winpath "$SOURCE_ABS") /Fo$(winpath "$BUILD_DIR")\\" + + wine cmd /c "$CMD" 2>&1 | + grep -v '^[0-9a-f]\{4\}:err:' | + strip_prefix "$(winpath "$REPO_ROOT")\\" + + status="${PIPESTATUS[0]}" + echo + echo "== cl.exe exit status: $status ==" + return "$status" +} + +# Allow `source scripts/vc6-probe.sh` (e.g. from tests) to pull in +# winpath/strip_prefix without running the probe itself. +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + run_probe "$@" + exit "$?" +fi diff --git a/scripts/vc6-probe.test.sh b/scripts/vc6-probe.test.sh new file mode 100755 index 0000000..4ad8128 --- /dev/null +++ b/scripts/vc6-probe.test.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# +# Regression tests for scripts/vc6-probe.sh. +# +# Covers the two failure modes that made the probe's exit status and +# output untrustworthy: +# 1. strip_prefix() must remove a literal Windows-style path prefix +# (backslash-separated) from probe output. A naive sed/glob +# pattern silently no-ops on this because `\h`, `\u`, etc. are +# consumed as escapes instead of literal characters. +# 2. The reported exit status must be cl.exe's real status even +# though its output passes through `grep` and `strip_prefix` in a +# pipeline. +# +# Part 2 additionally runs the real probe against a deliberately +# broken translation unit when wine + MSVC600 are available, so the +# check exercises the actual pipeline, not just the helper functions. +# +# Usage: scripts/vc6-probe.test.sh + +set -u + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PROBE="$REPO_ROOT/scripts/vc6-probe.sh" + +fail=0 +pass() { echo "ok - $1"; } +bad() { echo "not ok - $1"; fail=1; } + +# --- unit test: strip_prefix removes a literal backslash-heavy prefix --- + +# shellcheck source=scripts/vc6-probe.sh +source "$PROBE" + +prefix='Z:\home\ubuntu\projects\python-win9x\' +input="Z:\\home\\ubuntu\\projects\\python-win9x\\cpython\\Programs\\python.c(1) : error C2061" +expected="cpython\\Programs\\python.c(1) : error C2061" +actual="$(printf '%s\n' "$input" | strip_prefix "$prefix")" + +if [ "$actual" = "$expected" ]; then + pass "strip_prefix removes literal backslash-separated prefix" +else + bad "strip_prefix removes literal backslash-separated prefix (got: [$actual], want: [$expected])" +fi + +# A line that does not start with the prefix must be passed through +# unchanged (no partial/garbled matches). +input2="unrelated line with no prefix" +actual2="$(printf '%s\n' "$input2" | strip_prefix "$prefix")" +if [ "$actual2" = "$input2" ]; then + pass "strip_prefix leaves non-matching lines untouched" +else + bad "strip_prefix leaves non-matching lines untouched (got: [$actual2])" +fi + +# --- unit test: pipeline exit status reflects the first command, not +# grep/sed-equivalent downstream stages --- + +fake_failure() { return 7; } +fake_failure 2>&1 | grep -v '^nomatch' | strip_prefix "$prefix" >/dev/null +status="${PIPESTATUS[0]}" +if [ "$status" -eq 7 ]; then + pass "PIPESTATUS[0] reports the producing command's exit status through the filter pipeline" +else + bad "PIPESTATUS[0] reports the producing command's exit status through the filter pipeline (got: $status)" +fi + +# --- integration test: run the real probe against a broken translation +# unit, when the toolchain is actually available. Skipped (not +# failed) otherwise, since CI/dev boxes without wine+MSVC600 +# cannot exercise this. --- + +MSVC600_ROOT="${MSVC600_ROOT:-$REPO_ROOT/MSVC600}" +if command -v wine >/dev/null 2>&1 && [ -e "$MSVC600_ROOT/VC98/Bin/CL.EXE" ]; then + BAD_SRC="$REPO_ROOT/cpython/Programs/_vc6_probe_test_bad.c" + printf 'this is not valid C !!! ###\n' > "$BAD_SRC" + trap 'rm -f "$BAD_SRC"' EXIT + + out="$("$PROBE" cpython/Programs/_vc6_probe_test_bad.c 2>&1)" + real_status=$? + + if [ "$real_status" -ne 0 ]; then + pass "probe reports nonzero exit for a broken translation unit" + else + bad "probe reports nonzero exit for a broken translation unit (got: $real_status)" + fi + + if printf '%s' "$out" | grep -q 'error C'; then + pass "probe output surfaces the compiler diagnostic" + else + bad "probe output surfaces the compiler diagnostic" + fi + + if printf '%s' "$out" | grep -q 'Z:\\home'; then + bad "probe output leaks an unstripped absolute Wine path" + else + pass "probe output does not leak an unstripped absolute Wine path" + fi +else + echo "skip - wine/MSVC600 not available; skipping live probe integration check" +fi + +if [ "$fail" -ne 0 ]; then + echo "FAILED" + exit 1 +fi +echo "ALL TESTS PASSED"