port: add MSVC600 CPython compatibility layer

This commit is contained in:
RayPals
2026-08-17 17:35:32 +00:00
parent 3f15bfa5f7
commit a7a9560e5c
13 changed files with 2652 additions and 35 deletions
+36
View File
@@ -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 <stdbool.h>, 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. <inttypes.h> (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 <inttypes.h>
#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);
}
@@ -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;
}
+162
View File
@@ -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;
}
+54
View File
@@ -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);
}
+31
View File
@@ -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 <stdbool.h> 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 <stdbool.h>` resolves to via the VC6 probe
* scripts' include path.
*/
#include <stdbool.h>
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;
}
+104
View File
@@ -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: <repo>/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
+337
View File
@@ -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 <inttypes.h>
#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 <stdbool.h>,
# 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 <stdbool.h>
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
# <stdbool.h> 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* <stdbool.h> 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"
+107
View File
@@ -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: <repo>/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
+107
View File
@@ -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"