Skip to content

parity: say why a runner failed, then fix the five Windows failures it was hiding - #1104

Merged
youknowone merged 10 commits into
mainfrom
win-work
Aug 9, 2026
Merged

parity: say why a runner failed, then fix the five Windows failures it was hiding#1104
youknowone merged 10 commits into
mainfrom
win-work

Conversation

@youknowone

@youknowone youknowone commented Aug 7, 2026

Copy link
Copy Markdown
Owner

A failed pyre/extra_tests/parity_tests job named none of what failed in it.
This one
ends:

  winreg_key_lifecycle.py              cpython=OK dynasm=OK cranelift=OK
  zip_identity_python314.py            cpython=OK dynasm=OK cranelift=OK

##[error]Process completed with exit code 1.

So the runner now says why, and then everything it was failing to say — five
scripts, red on every Windows runner — is fixed.

1. The runner reports where a CI log is read

The reason was in the log, 200 lines above the run's own header. The
per-failure detail and the N failure(s) count went to stderr while the
213-row table went to stdout, and a piped stdout is block-buffered where
stderr is not, so the report overtook everything it was meant to explain and
the last line before the non-zero exit was a passing row.

  • The report is printed last, on stdout, and echoes each child's stderr
    verbatim rather than a repr of the whole thing on one line — a traceback
    only reads as one when its line breaks survive.
  • Each failing row carries its one-line verdict beneath it.
  • ::error file= annotations under GITHUB_ACTIONS, one per failure,
    carrying the verdict and the exception line. They show on the pull request
    without opening the log.
  • stdout is line-buffered, so a run that dies mid-way names the scripts it
    got through.
  • stdout is pinned to UTF-8. This one bit during verification: the report
    echoes a child's stderr, and these scripts are largely about names no console
    codepage can spell, so printing one raised UnicodeEncodeError out of the
    runner
    instead of the failure it was explaining — the same no-reason death,
    one level up.

_run returns the reason and the stderr as separate values, which the new
Failure tuple carries; nothing about what counts as a pass moves.

It works: the run this branch was rebased over reported its own failure as
surrogate_name_messages.py [cpython]: exited 1 … AssertionError: '<function q\udcffn at 0x000002A594587E20>', traceback and all, which is §5 below.

2. The failure it was hiding

Two independent things, both Windows:

CPython has no os.pathconf there. The reference failed the script at
line 96 before any backend could, and comparing a backend against a failing
reference measures nothing. That section is now gated on the name existing —
hasattr, not a platform list, because what it needs is the name.

os.utime refused every time before 1970. The host call takes its times
as a Duration, which has no second below its epoch at all, so
u64::try_from(sec) was the whole pre-epoch range's refusal.

#1107 landed a Windows SetFileTime path over the same lines while this
branch was out, and the rebase takes its structure — CreateFileW with
FILE_WRITE_ATTRIBUTES and FILE_FLAG_BACKUP_SEMANTICS, which is what
rposix.py:1558-1575 opens with and what lets the name open a directory or a
read-only file. What is kept from this side is the arithmetic: r_longlong
in rwin32file.py:320-323, and the __int64 of the C it transcribes, both
wrap. A checked conversion answers a ValueError for a tick count neither
runtime produces, and by then anything no time_t can hold has already been
refused with the OverflowError below.

3. What measuring it turned up

Every row below was measured against CPython 3.14 on the same host, before
and after:

was now (== CPython 3.14)
utime(p, times=(-5.0, -6.0)) ValueError: utime: timestamp out of range writes it; st_mtime_ns == -6_000_000_000
utime(p, ns=(-1, -1)) same refusal -100 — a FILETIME's granularity
utime(p, ('a', 'b')) ValueError: could not convert string to float TypeError: argument must be int or float, not str
utime(p, (1e30, 0)) ValueError: utime: timestamp out of range OverflowError: timestamp out of range for platform time_t
utime(p, (2**200, 0)) the same, via a float that rounded it the same OverflowError, from the exact integer
utime(p, (nan, 0)) ValueError: utime: timestamp out of range ValueError: Invalid value NaN (not a number)
utime(p, (1,)) utime: 'times' must be a tuple of two ints ... must be either a tuple of two ints or None
utime(p, ns=(2**80, 0)) OverflowError written
truncate(p, 2**64) int too large to convert to int int too big to convert
st_mtime_ns of a file dated 30000 -5443715538058477568 880000000000000000000

Three of those are worth their own sentence:

  • ns=2**80. split_py_long_to_s_and_ns splits with divmod before it
    narrows anything, so a nanosecond count too wide for a time_t is refused
    only when the second it names is — and 2**80 ns is a second that fits.
    Dividing after the narrowing turned away the whole range. Going through
    divmod is also what answers for ns=('a', 'b').
  • truncate. The Windows arms read their length with a bare int_w: no
    __index__, and the wrong message. Both now go through truncate_length_w,
    hoisted out of the unix arm, which names the C type the platform's own
    Py_off_t_converter names.
  • st_*_ns. The sec * 1_000_000_000 product was taken in i64, which
    runs out in 2262 — not an exotic date for a filesystem whose FILETIME
    reaches the year 30828. It is taken in i128 now and the field is an int of
    whatever width it needs.

parity_tests/os_utime_pathconf_truncate pins all of it and no longer skips
its negative-time section on Windows; only the exact-nanosecond value is
platform-dependent there.

4. The three failures it was hiding everywhere else

The same job on #1109
ends the same way — a passing row, then exit code 1 — over three more scripts,
all Windows, all main's and independent of each other:

  • keyboard_interrupt_exit_status. A Win32 process has no SIGINT to die
    of. app_main.py restores SIG_DFL and calls raise(SIGINT) saying the MSVC
    runtime then exits with STATUS_CONTROL_C_EXIT; measured, that pair returns
    and the process ends with status 3 — under CPython too. raise never
    terminating meant terminate_by_sigint fell through to process::abort,
    whose status 3 was what the fixture read. Windows exits with
    STATUS_CONTROL_C_EXIT directly now.
  • builtin_module_loader_spec. _sysconfig.config_vars() answered an empty
    dict, but sysconfig._init_non_posix subscripts Py_GIL_DISABLED and
    Py_DEBUG, so under os.name == 'nt' an absent key is a KeyError and not
    the None the .get() readers take. Behind it, sys._vpath and
    sys.getwindowsversion were both absent — the latter now a five-field
    sequence with five named-only fields over it, off
    host_env::windows::get_windows_version, matching CPython 3.14 field for
    field bar build (see the commit for why kernel32's file version is the
    honest one for an unmanifested binary).
  • frame_clear_finalization imported resource for one CPU-time bound at
    the end; it is POSIX-only, absent from CPython on Windows too, so the import
    took all six checks off the platform and left the reference failing beside the
    backends. time.process_time is the same measurement and is everywhere.

5. Every repr's address was wrong on Windows

surrogate_name_messages was red on Windows with cpython=FAIL — the
reference failing it, so the backends were being compared against a failing
oracle. It asserts "<function %s at 0x%x>" % (S, id(f)), and that is the glibc
spelling.

PyUnicode_FromFormat's %p hands the pointer to the platform's own printf
and normalizes only the prefix — guaranteed to start with a literal 0x
"regardless of what the platform's printf yields". Everything after it is the
platform's: the MSVC runtime pads to the pointer width and uppercases, glibc
does neither. Rust's {:p}, {:?} on a raw pointer, and a hand-written
0x{:x} are all only ever the second spelling, so every address-bearing
repr was wrong there. Measured against CPython 3.14 on the same host, all of
them disagreed:

was now (== CPython 3.14)
repr(f) <function f at 0x1b7af7ffcc0> <function f at 0x000001B7AF7FFCC0>

…and the same for object, generator, coroutine, async_generator, bound and
built-in method, method-wrapper, cell, weakref, memoryview, ContextVar, Token,
code, frame and both _thread locks — fifteen kinds, now through one
display::repr_addr, verified shape-identical. _pickle's cyclic-object
message names an address the same way and is the one such site that is not a
repr.

The frame repr's file '...' is left raw: pypy/interpreter/pyframe.py:851
interpolates '%s' where CPython's %R escapes the backslashes a Windows path
is full of. That is a parity-source disagreement rather than this fix's
business.

6. And the failure that was already there

synth/pypy_type_surface is red on main itself, on all three runners and
byte-identically on all three backends — bridges_compiled 102 -> 5, guard_failures 20497 -> 1011 in
ubuntu,
macos
and windows
— so the branch could not be green while it stood.

The recorded 102/20497 are what the Cls.__name__ fold measured while its
metaclass guard could not be discharged: the guard reads the raw w_class
slot, the fast path answers through typedef::type's gettypefor(ob_type)
fallback, and a receiver reached through that fallback got a
guard_value(NULL, type) that failed on every execution and that nothing later
discharged. Taking the fixture apart puts every one of the 97 excess bridges in
check_descriptor_kinds, which reads type(type.__dict__[name]).__name__; the
getset_descriptor type object is the one class that reaches it, built lazily
from inside the init loop so it never enters the registry the post-loop sweep
walks to stamp the slot.

#1106 landed the fold's narrowing while this branch was rebasing, so the
cherry-pick dropped out; what is left is the baseline, re-recorded here from
what the three runners and a local windows run all report rather than from a
prediction.

Also left of it is a debug_assert on the helper: is_exact_builtin_instance
and is_plain_int1 both read a null w_class as a second spelling of "exact
builtin", so an operand admitted under that spelling and pinned against the
canonical gets a guard its own recorded operand fails. Measured across
bench/synth, bench and parity_tests — 1217 pins over 131 files — every
site already establishes what it pins, and nothing checked that they did.

7. The review round

  • sys.getwindowsversion built its structseq type inside the call, so
    type(sys.getwindowsversion()) is type(sys.getwindowsversion()) read False
    where CPython reads True. It was the one of pyre's ten structseq types not
    already caching in a OnceLock; a probe over every structseq both runtimes
    carry reports the types equal for all nine others and for all of CPython's,
    so this constructor was the whole divergence. structseq_type_identity pins
    the property and fails on the binary built before the fix.
  • A timed-out child's stderr was bytes. subprocess.run(text=True) decodes
    what communicate() returns; the timeout path raises with the raw chunks it
    had joined. Reporting those would have printed one b'...\n...' line — the
    shape §1 exists to remove.
  • Two utime assertions could only hold on NTFS, and the reason neither
    was caught is that the parity step has never reached ubuntu or macos:
    check.py runs first in that job and has been failing, so the suite never
    starts. ns=(2**62, 2**62) expected 2**62 rounded to a FILETIME tick, and
    (8.8e11, 8.8e11) names the year 29880 — an APFS timestamp is itself an
    int64 of nanoseconds so 2262 bounds it too, and ext4 stops in 2446. Both go
    through one storable() now, and the far date is held to the identity the
    i128 widening buys wherever the write succeeds.

Verification

Rebased onto a7eb493079; re-extracted LLBC before every measurement a stale
image could have fabricated.

  • extra_tests/parity_tests on Windows — 214/214 on both backends, bar
    builtin_module_loader_spec under a local CPython with no test package
    installed; CI's reference has it and reported cpython=OK. It was 208/213
    before, five scripts red on every Windows runner.
  • pyre/check.py410/410 on dynasm and 410/410 on cranelift, both
    all-green. synth/type_name_attr_fold still reads its own 5/0/1/4, so
    jit(wasm): drop the wasm32 arm of the self-recursive root-bridge inline #1106's decline is narrow enough that the fold keeps applying wherever its
    guard holds.
  • cargo test --all --no-default-features --features dynasm, and the cranelift
    subset CI re-runs after it — both clean. cargo fmt --all -- --check clean.
  • four ad-hoc probe scripts covering 30 utime/truncate/stat shapes,
    diffed line by line against CPython 3.14 — 0 differences.

Summary by CodeRabbit

  • New Features

    • Added Windows support for sys.getwindowsversion(), locale operations, and platform-specific filesystem behavior.
    • Improved handling of negative, high-precision, and far-future file timestamps.
    • Standardized object representations across platforms, including Windows address formatting.
  • Bug Fixes

    • Improved timestamp validation, overflow handling, truncation, and filesystem metadata reporting.
    • Windows interrupt termination now exits with the expected control-C status.
    • Added clearer test failure details, timeout diagnostics, and CI annotations.
  • Tests

    • Expanded parity coverage for locale behavior, timestamp APIs, and struct-sequence types.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Platform-aware object representations

Layer / File(s) Summary
Shared address formatting and consumers
pyre/pyre-interpreter/src/display.rs, pyre/pyre-interpreter/src/function.rs, pyre/pyre-interpreter/src/module/..., pyre/pyre-interpreter/src/typedef.rs
Object addresses now use repr_addr with platform-specific formatting.
Representation parity coverage
pyre/extra_tests/parity_tests/surrogate_name_messages.py
Representation assertions now accept platform-specific address formats.

Filesystem and Windows parity

Layer / File(s) Summary
Filesystem runtime handling
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
utime, stat timestamps, and truncation now use platform-aware validation and wide timestamp arithmetic.
Windows system and locale behavior
pyre/pyre-interpreter/src/module/sys/vm.rs, pyre/pyre-interpreter/src/module/_locale/interp_locale.rs, pyre/pyre-interpreter/src/importing.rs
Windows now exposes version data and path configuration. Locale constants and _sysconfig values now reflect platform support.
Parity coverage
pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py, pyre/extra_tests/parity_tests/locale_categories.py, pyre/extra_tests/parity_tests/structseq_type_identity.py
Tests cover timestamps, path configuration, locale behavior, and structseq identity.

Parity runner diagnostics

Layer / File(s) Summary
Structured execution results
pyre/extra_tests/parity_tests/run.py, pyre/cpython_tests/run.py
Runners preserve timeout details, stderr, output encoding, and failure digests.
Failure reporting and exit status
pyre/extra_tests/parity_tests/run.py
The parity runner consolidates failures, emits CI annotations, and returns failure status when required.

JIT diagnostics and baselines

Layer / File(s) Summary
Exact-class guard validation
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
Debug builds verify the concrete operand class before exact-class guard emission.
Synthetic JIT statistics
pyre/bench/synth/*.jitstats
Benchmark counters record updated bridge and guard-failure values and zero retraces.

Runtime compatibility

Layer / File(s) Summary
Platform-specific termination
pyre/pyrex/src/lib.rs
Windows termination uses STATUS_CONTROL_C_EXIT; non-Windows termination retains signal handling.
Portable process-time measurement
pyre/extra_tests/parity_tests/frame_clear_finalization.py
The benchmark uses time.process_time() instead of resource.getrusage().

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Poem

A rabbit checks each address bright,
With Windows caps and Unix light.
Timestamps stretch; failures speak.
JIT counters find the path they seek.
Across the burrow, tests align.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: improved parity runner failure reporting and fixes for five Windows-specific failures.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch win-work

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8dd0b49b32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +150 to +151
partial = expired.stderr or ""
return False, f"timed out after {TIMEOUT}s", partial

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decode captured timeout stderr before reporting it

When a parity script exceeds the 30-second timeout after writing to stderr, TimeoutExpired.stderr is bytes even though subprocess.run was called with text=True. Passing it through here makes _report and _annotate render each line as a bytes representation such as b'partial stderr', rather than preserving the child's decoded stderr as promised. Decode the partial output with UTF-8 and replacement handling before storing it in Failure.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 24807b7).
Updated: 2026-08-09T03:28:02.427Z

Files in the reviewed diff
pyre/cpython_tests/run.py
pyre/extra_tests/parity_tests/frame_clear_finalization.py
pyre/extra_tests/parity_tests/locale_categories.py
pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
pyre/extra_tests/parity_tests/run.py
pyre/extra_tests/parity_tests/structseq_type_identity.py
pyre/extra_tests/parity_tests/surrogate_name_messages.py
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/display.rs
pyre/pyre-interpreter/src/function.rs
pyre/pyre-interpreter/src/importing.rs
pyre/pyre-interpreter/src/module/_contextvars/mod.rs
pyre/pyre-interpreter/src/module/_ctypes/cdata.rs
pyre/pyre-interpreter/src/module/_locale/interp_locale.rs
pyre/pyre-interpreter/src/module/_pickle/pickler.rs
pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-interpreter/src/module/thread/mod.rs
pyre/pyre-interpreter/src/pycode.rs
pyre/pyre-interpreter/src/pyframe.rs
pyre/pyre-interpreter/src/typedef.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyrex/src/lib.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs:720 ↔ pypy/module/_weakref/interp__weakref.py:187-192 — Pyre renders a live weakref only as "; to '<type>'", while PyPy additionally includes the referent’s nonempty __name__: "; to '<type>' (<name>)". This change only alters address formatting, so the omission predates the patch.

  • pyre/pyre-interpreter/src/display.rs:28 ↔ pypy/interpreter/baseobjspace.py:96-117 — Pyre’s Unix address rendering remains non-padded (0x{addr:x}), while PyPy’s getaddrstring() emits a fixed-width lowercase hexadecimal identity. The new helper preserves the prior Unix behavior.

4. Structural adaptations

  • pyre/pyre-interpreter/src/module/sys/vm.rs:1382 ↔ pypy/module/sys/vm.py:264-268 — Pyre represents the five getwindowsversion() named-only fields as contiguous structseq extras; PyPy assigns sparse logical indices 10–14. The public attributes and five-item tuple body are retained, but Pyre’s generic Rust structseq representation cannot encode PyPy’s sparse field-index metadata directly.

  • pyre/pyre-interpreter/src/module/sys/vm.rs:1409 ↔ pypy/module/sys/vm.py:294-296 — Pyre returns (major, minor, build) for platform_version; the local PyPy 3.11-era implementation explicitly supplies None. This is a Python 3.14/CPython compatibility adaptation.

  • pyre/pyre-interpreter/src/display.rs:25-29 ↔ pypy/interpreter/baseobjspace.py:96-117 — Windows representations use CPython/MSVC-style fixed-width uppercase pointers, whereas PyPy’s interpreter-level identity formatting is lowercase. This is a platform/CPython-representation adaptation, applied consistently to the changed repr call sites.

  • pyre/pyrex/src/lib.rs:1258 ↔ pypy/interpreter/app_main.py:1146-1154 — Pyre exits directly with STATUS_CONTROL_C_EXIT on Windows rather than restoring SIG_DFL and raising SIGINT. This is a Rust/Windows process-termination adaptation intended to preserve the externally observed exit status.

@youknowone youknowone changed the title extra_tests: report the parity runner's failures where a CI log is read parity: say why a runner failed, then fix what it was hiding — Windows pre-epoch utime Aug 7, 2026
@youknowone youknowone changed the title parity: say why a runner failed, then fix what it was hiding — Windows pre-epoch utime parity: say why a runner failed, then fix the four Windows failures it was hiding Aug 8, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50abf5b3de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# `ns` is split with divmod before anything is narrowed, so a nanosecond count
# too wide for a `time_t` is only refused when the SECOND it names is.
os.utime(p, ns=(2**62, 2**62))
check(os.stat(p).st_mtime_ns == 4611686018427387900, os.stat(p).st_mtime_ns)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve full nanosecond precision on Unix

When this all-platform fixture runs on a Unix filesystem with nanosecond precision, the requested value remains exactly 2**62, not the Windows-specific 100-ns-rounded value used here. Running the fixture with CPython 3.14.4 on Linux/ext4 produced 4611686018427387904, so the reference runner fails at this assertion before any pyre backend is compared; make the expected value platform-dependent as the earlier -1ns check does.

Useful? React with 👍 / 👎.

# Nanoseconds run out of an `int64` in 2262, and a file may carry a later time
# than that — `st_mtime_ns` is the number it is rather than a wrap.
os.utime(p, (8.8e11, 8.8e11))
check(os.stat(p).st_mtime_ns == 880_000_000_000_000_000_000, os.stat(p).st_mtime_ns)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Choose a timestamp the filesystem can represent

On Linux/ext4, 8.8e11 seconds is beyond the filesystem's timestamp range, so CPython 3.14.4 clamps it and reports 15032385535000000000 ns rather than this value. After the preceding precision assertion is fixed, the CPython reference will still fail here; use a post-2262 timestamp within common Unix filesystem ranges so the test isolates Python-integer widening instead of filesystem limits.

Useful? React with 👍 / 👎.

pyre_object::PY_NULL,
)
})?;
let cls = crate::_structseq::make_struct_seq_with_extra(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cache the getwindowsversion structseq type

On Windows, every call constructs a fresh sys.getwindowsversion class, so type(sys.getwindowsversion()) is type(sys.getwindowsversion()) is false and an object from one call is not an instance of the next call's type. Move this type creation to a process-global OnceLock<usize>, as the existing stat_result and struct_time implementations do, and reuse it for every result.

AGENTS.md reference: AGENTS.md:L148-L155

Useful? React with 👍 / 👎.

@youknowone youknowone changed the title parity: say why a runner failed, then fix the four Windows failures it was hiding parity: say why a runner failed, then fix the five Windows failures it was hiding Aug 8, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93eb41e6c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +848 to +850
pyre_object::w_dict_store(
vars,
pyre_object::w_str_new(name),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Root the config-vars dictionary across key allocations

When _sysconfig.config_vars() runs under nursery pressure, w_str_new(name) can trigger a moving collection after vars has been copied into the call expression, leaving w_dict_store with the dictionary's pre-collection address; the store itself can also move the dictionary before the next iteration or final return. Pin the dictionary for the whole construction and reload it from the shadow stack, as the analogous dictionary-building path in call.rs does.

Useful? React with 👍 / 👎.

sec: n.div_euclid(1_000_000_000),
nsec: n.rem_euclid(1_000_000_000),
sec,
nsec: crate::builtins::space_index_w(w_nsec)?,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the divmod result rooted while converting its elements

For a valid ns object whose __divmod__ returns user-defined quotient and remainder objects, converting w_sec can execute __index__ and trigger a moving GC. Neither the returned tuple nor w_nsec is rooted, so this subsequent conversion can dereference the remainder's stale pre-collection address; pin the tuple or both elements and reload the remainder after converting the quotient.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py`:
- Around line 103-106: Update the os.utime timestamp case and its st_mtime_ns
assertion to use a filesystem-storable value just beyond the i64 nanosecond
boundary, such as 10_000_000_000 seconds, or gate the assertion on filesystem
capability; preserve validation of the resulting nanosecond timestamp without
requiring the unsupported 8.8e11-second value.

In `@pyre/extra_tests/parity_tests/run.py`:
- Around line 149-151: Update the subprocess.TimeoutExpired handling in the
timeout path to decode expired.stderr before returning it, while preserving the
empty fallback when stderr is absent. Ensure the returned Failure.stderr value
is text so downstream reports and annotations receive clean traceback lines
rather than a bytes representation.

In `@pyre/extra_tests/parity_tests/surrogate_name_messages.py`:
- Around line 31-32: Replace all remaining percent-style formatting in this test
module, including the ValueError message and the address formatting in at(),
with format-specifier syntax. Preserve the existing output text,
platform-specific width, and casing: uppercase fixed-width addresses on Windows
and lowercase addresses elsewhere.

In `@pyre/pyre-interpreter/src/module/sys/vm.rs`:
- Around line 1377-1387: Update the sys module initialization and
getwindowsversion result construction so make_struct_seq_with_extra creates the
sys.getwindowsversion struct-sequence type once during initialization. Store
that type in the module state, then reuse it for every subsequent
sys.getwindowsversion() call instead of recreating it per result.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bf7f3a53-5759-4ee2-bc0c-f149dfd936d8

📥 Commits

Reviewing files that changed from the base of the PR and between a7eb493 and 93eb41e.

📒 Files selected for processing (23)
  • pyre/bench/synth/pypy_type_surface.cranelift.jitstats
  • pyre/bench/synth/pypy_type_surface.dynasm.jitstats
  • pyre/bench/synth/pypy_type_surface.wasm.jitstats
  • pyre/extra_tests/parity_tests/frame_clear_finalization.py
  • pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
  • pyre/extra_tests/parity_tests/run.py
  • pyre/extra_tests/parity_tests/surrogate_name_messages.py
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/display.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/importing.rs
  • pyre/pyre-interpreter/src/module/_contextvars/mod.rs
  • pyre/pyre-interpreter/src/module/_ctypes/cdata.rs
  • pyre/pyre-interpreter/src/module/_pickle/pickler.rs
  • pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-interpreter/src/module/thread/mod.rs
  • pyre/pyre-interpreter/src/pycode.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyrex/src/lib.rs

Comment thread pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py Outdated
Comment thread pyre/extra_tests/parity_tests/run.py
Comment on lines +31 to +32
return "0x%0*X" % (2 * (8 if sys.maxsize > 2**32 else 4), id(obj))
return "0x%x" % id(obj)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
ruff check pyre/extra_tests/parity_tests/surrogate_name_messages.py --select UP031

Repository: youknowone/pyre

Length of output: 4646


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file sections and the dynamic assertions/proposed substitutions.
sed -n '1,150p' pyre/extra_tests/parity_tests/surrogate_name_messages.py | nl -ba | sed -n '1,150p'

python3 - <<'PY'
# Probe the exact current format semantics and proposed replacements for representative values.
class S:
    def __init__(self, value):
        self.value = value
    def __repr__(self):
        return f"<S({self.value:`#x`})>"

def current_at(obj):
    import sys
    if sys.platform == "win32":
        return "0x%0*X" % (2 * (8 if sys.maxsize > 2**32 else 4), id(obj))
    return "0x%x" % id(obj)

def new_at(obj):
    import sys
    width = 2 * (8 if sys.maxsize > 2**32 else 4)
    return f"0x{id(obj):0{width}X}"

for platform, old, new in [(None, current_at, new_at), ("win32", current_at, new_at)]:
    print("platform:", platform)
    for obj in [object(), object()]:
        print(old(obj), "==", new(obj))
PY

Repository: youknowone/pyre

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -u

echo "== relevant file section =="
sed -n '1,150p' pyre/extra_tests/parity_tests/surrogate_name_messages.py

echo
echo "== semantic probe =="
python3 - <<'PY'
import sys

class S:
    def __init__(self, value):
        self.value = value
    def __repr__(self):
        return f"<S({self.value:`#x`})>"

def current_at(obj):
    if sys.platform == "win32":
        return "0x%0*X" % (2 * (8 if sys.maxsize > 2**32 else 4), id(obj))
    return "0x%x" % id(obj)

def new_at(obj):
    width = 2 * (8 if sys.maxsize > 2**32 else 4)
    return f"0x{id(obj):0{width}X}"

for platform in [None, "win32"]:
    print("platform:", platform)
    for obj in [object(), object()]:
        print(old := current_at(obj), "==", new := new_at(obj))
        if old != new:
            raise SystemExit("at() replacement mismatch")
PY

Repository: youknowone/pyre

Length of output: 4780


Replace the remaining percent-formatted assertions with format specifiers.

Run Ruff UP031 on any leftover % formatting, including the ValueError: %s\n case, and update the assertions to keep the current output text. For at(), preserve the platform-specific width and case semantics: use an upper-case fixed-width address on Windows, and the lower-case address elsewhere.

🧰 Tools
🪛 Ruff (0.16.1)

[warning] 31-31: Use format specifiers instead of percent format

(UP031)


[warning] 32-32: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/extra_tests/parity_tests/surrogate_name_messages.py` around lines 31 -
32, Replace all remaining percent-style formatting in this test module,
including the ValueError message and the address formatting in at(), with
format-specifier syntax. Preserve the existing output text, platform-specific
width, and casing: uppercase fixed-width addresses on Windows and lowercase
addresses elsewhere.

Source: Linters/SAST tools

Comment thread pyre/pyre-interpreter/src/module/sys/vm.rs Outdated
@youknowone

Copy link
Copy Markdown
Owner Author

Thanks — three of the four were real, and one of them was worse than reported.

sys.getwindowsversion rebuilding its type: fixed, and it was a behaviour bug. Building the struct-sequence type inside the call meant type(sys.getwindowsversion()) is type(sys.getwindowsversion()) read False where CPython reads True. Probing every structseq both runtimes carry: pyre reports the types equal for all nine others and CPython for all of its, so this one constructor was the whole divergence — and it was the only one of pyre's ten that did not already cache in a OnceLock (stat_result, terminal_size, uname_result, statvfs_result, waitid_result, times_result, struct_time, _ExceptHookArgs, UnraisableHookArgs all do). It now does the same. The cache is a bare usize the GC cannot see, which is the established pattern rather than a new bet — checked rather than assumed: 200 os.stat answers dropped across forced collections plus 60MB of churn leave the cached type identical and its fields readable.

New parity_tests/structseq_type_identity.py pins the property for every structseq the host carries. It fails on the binary built before the fix, naming exactly sys.getwindowsversion.

Timed-out child's stderr: fixed. subprocess.run(text=True) decodes what communicate() returns; the timeout path raises with the raw chunks it had joined, so it is bytes on POSIX and str on Windows. It is decoded now — reporting the bytes would have printed one b'...\n...' line, which is the unreadable shape this PR exists to remove.

The 8.8e11-second utime: fixed, and there was a second one beside it. Both assertions could only ever have held on NTFS, and the reason neither had been caught is that the parity step has never run on ubuntu or macoscheck.py runs first in that job and has been failing, so the suite never starts.

  • ns=(2**62, 2**62) expected 4611686018427387900, which is 2**62 rounded down to a FILETIME's 100ns tick; ext4 and APFS keep the nanosecond and answer ...904.
  • utime(p, (8.8e11, 8.8e11)) names the year 29880. No filesystem but NTFS reaches it — an APFS timestamp is an int64 of nanoseconds, so 2262 bounds it too, and ext4 stores a 34-bit second and stops in 2446.

Both spellings now come from one storable(), which also replaces the -1/-100 conditional above them. The far-date block asserts the identity the i128 widening actually buys — st_mtime_ns == int(st_mtime) * 1_000_000_000 — wherever the write succeeds, and the exact value only where the second survived the round trip.

f-strings in surrogate_name_messages: left alone. The file builds every expected repr with % formatting, including the assertions this branch did not touch, and nothing lints for it; converting one helper would make it the odd one out.

Verified after the change, with LLBC re-extracted first: check.py 410/410 on dynasm and 410/410 on cranelift, parity 213/213 on both backends (bar builtin_module_loader_spec under a local CPython with no test package), cargo test --all --features dynasm clean, cargo fmt --check clean.

The per-failure detail and the `N failure(s)` count went to stderr while the
213-row result table went to stdout.  A piped stdout is block-buffered and
stderr is not, so the whole report arrived in the log *above* the run's own
header, and the last thing before the runner's non-zero exit was a passing
row -- a failed job named none of what failed in it.

The report is printed last now, on stdout, and echoes each child's stderr
verbatim instead of a `repr` of the whole thing on one line: a traceback only
reads as one when its line breaks survive.  Each failing row also carries its
one-line verdict beneath it, and under `GITHUB_ACTIONS` every failure emits an
`::error file=` annotation carrying that verdict and the exception line, which
shows on the pull request without opening the log.

stdout is line-buffered so a run that dies mid-way names the scripts it got
through, and pinned to UTF-8 because the report echoes a child's stderr: these
scripts are largely about names no console codepage can spell, and printing
one of those raised `UnicodeEncodeError` out of the runner instead of the
failure it was in the middle of explaining.

`_run` returns the reason and the stderr as separate values, which `Failure`
carries.
`os.utime` on Windows turned away every time before 1970.  The host call
takes its times as a `Duration`, which has no second below its epoch at all,
so `u64::try_from(sec)` was the whole pre-epoch range's refusal.  A FILETIME
counts 100ns ticks from 1601-01-01, so shifting the epoch is what makes such
a second a positive tick count; `SetFileTime` is called here now, over the
handle `host_env::fs::open_write_with_custom_flags` opens, with the same
wrapping `__int64` arithmetic `time_t_to_FILE_TIME` is written in.

Measured against CPython 3.14 on the same host, `os.utime(p, times=(-5.0,
-6.0))` now reads back as -6_000_000_000 where it raised, and `ns=(-1, -1)`
as the -100 a FILETIME's granularity leaves.

Six more answers along the same argument, each measured against 3.14:

  ('a', 'b')     ValueError: could not convert string to float
                   -> TypeError: argument must be int or float, not str
  (1e30, 0)      ValueError: utime: timestamp out of range
                   -> OverflowError: timestamp out of range for platform time_t
  (2**200, 0)    the same, and by the exact integer rather than through a
                   float that rounded the seconds it could not hold
  (nan, 0)       ValueError: utime: timestamp out of range
                   -> ValueError: Invalid value NaN (not a number)
  (1,)           utime: 'times' must be a tuple of two ints
                   -> ... must be either a tuple of two ints or None
  ns=(2**80, 0)  OverflowError -> written.  `split_py_long_to_s_and_ns`
                   splits with `divmod` BEFORE it narrows anything, so a
                   nanosecond count too wide for a `time_t` is refused only
                   when the second it names is; 2**80 ns is a second that
                   fits.  Dividing after the narrowing turned away the range.
                   `divmod` is also what answers for `ns=('a', 'b')`.

`os.truncate`/`os.ftruncate` on Windows read their length with a bare
`int_w`: no `__index__`, and `int too large to convert to int` where
`Py_off_t_converter` says `int too big to convert`.  Both now go through
`truncate_length_w`, which is hoisted out of the unix arm and names the C
type the platform's converter names.

`st_atime_ns` and its two siblings took `sec * 1_000_000_000` in `i64`, which
runs out in 2262 -- a file dated later, which every FILETIME up to the year
30828 can be, read back as the wrap.  The product is taken in `i128` and the
field is an int of whatever width it needs.

`parity_tests/os_utime_pathconf_truncate` covers all of it and no longer
skips its negative-time section on Windows; only the exact-nanosecond value
is platform-dependent there.  Its `pathconf` section is now gated on the name
existing, because neither CPython nor this build carries `pathconf` on
Windows and the reference failed the script before any backend could -- which
is what made the whole script red on every Windows runner.
`is_exact_builtin_instance` (pyobject.rs:149-163) reads a null `w_class` as a
second spelling of "exact builtin", beside the one where the slot holds the
canonical type object, and `is_plain_int1` (listobject.rs:424-460) accepts both
for `int` and for a fits-int `W_LongObject`. `walker_guard_exact_w_class` reads
the slot and pins a single value, so an operand admitted under the null spelling
and pinned against the canonical gets a guard its own recorded operand fails —
and nothing writes the slot afterwards, so it fails on every execution without
converging, one bridge per `trace_eagerness` bucket. That is the shape
`try_walker_specialize_load_type_name_attr` shipped with, where the fold took
its metaclass from `typedef::type`'s `gettypefor(ob_type)` fallback while the
guard read the raw field.

The 40-odd other call sites establish the operand carries what they pin —
through `walker_exact_builtin_class`, which returns `None` on a null slot, or
through `is_plain_int1` / a local `is_exact_int` — but nothing checked that they
do. Measured across `bench/synth`, `bench` and `extra_tests/parity_tests`,
under a probe that reported the recorded slot against the pinned value at every
site: 1217 pins over 131 files, all carrying what they pin, and no site reaching
the guard without a concrete operand. The null spelling is reachable — `bool`,
`None`, functions, generators, iterators, sets and the itertools objects are all
built with a null slot, and `SMALL_INTS` is written that way behind
`WITHPREBUILTINT` — so what holds is that no admitting predicate currently pairs
one with a canonical pin, not that it could not.

`debug_assert!` rather than a decline: there is no live site to decline, release
codegen is unchanged, and the next occurrence fails loudly instead of costing a
20x jit-stats drift that takes a baseline diff to notice.
… none

`pyre/check.py (windows-latest)` on #1109 ends on a passing row and exit code 1;
the three scripts it failed on are 200 lines up, in the stderr the neighbouring
commit here moves. They are main's, not that PR's, and independent of each
other.

**`keyboard_interrupt_exit_status`.** A Win32 process has no SIGINT to die of.
`app_main.py:1146-1151` restores `SIG_DFL` and calls `raise(SIGINT)`, saying the
MSVC runtime then exits with `STATUS_CONTROL_C_EXIT`; measured, that pair
returns through the CRT's default action and the process ends with status 3 —
`signal.signal(SIGINT, SIG_DFL); signal.raise_signal(SIGINT)` under CPython
exits 3 as well, while an uncaught `KeyboardInterrupt` there exits 0xC000013A.
`raise` never terminates, so `terminate_by_sigint` fell through to
`process::abort`, whose status 3 is what the fixture read. Windows exits with
`STATUS_CONTROL_C_EXIT` directly; both callers have already finalized.

**`builtin_module_loader_spec`.** Two missing names, both reached from
`test.support`'s import:
- `_sysconfig.config_vars()` answered an empty dict. `sysconfig._init_non_posix`
  SUBSCRIPTS `Py_GIL_DISABLED` and `Py_DEBUG` to spell `ABIFLAGS`, so under
  `os.name == 'nt'` an absent key is a `KeyError` out of the first
  `get_config_var` rather than the `None` the `.get()` readers take. Both are 0,
  which is what an empty `sys.abiflags` already says. `EXT_SUFFIX` and `SOABI`,
  the other two keys the call carries, name an extension ABI that
  `_imp.extension_suffixes()` says does not exist here, so they stay absent.
- `sys.getwindowsversion` was absent and `_init_config_vars` subscripts
  `sys._vpath` beside it. The version is a five-field sequence with five
  named-only fields over it, built the way `os.stat_result` carries its `st_*_ns`
  extras, off `host_env::windows::get_windows_version`. Every field matches
  CPython 3.14 on the same host except `build`: the sequence reports kernel32's
  file version, because `GetVersionEx` answers with the version an unmanifested
  binary is shimmed to, and `platform_version` — the field that exists because of
  that shimming — agrees with it here instead of correcting it.

**`frame_clear_finalization`** imported `resource` for one CPU-time bound at the
end. It is a POSIX module, absent from CPython on Windows too, so the import
took all six checks off the platform and left the reference failing beside the
backends. `time.process_time` is the same measurement and is everywhere.

extra_tests/parity_tests: 213/213 on both backends, bar
`builtin_module_loader_spec` under a local CPython with no `test` package
installed — the runner that CI uses has it and reported `cpython=OK`.
`PyUnicode_FromFormat`'s `%p` hands the pointer to the platform's own `printf`
and normalizes only the prefix — guaranteed to start with a literal `0x`
"regardless of what the platform's printf yields". The platforms disagree about
everything after it: the MSVC runtime pads to the pointer width and uppercases,
glibc does neither. So on Windows CPython reads
`<function f at 0x000001B7AF7FFCC0>` where it reads `<function f at
0x1b7af7ffcc0>` elsewhere, and Rust's `{:p}` — along with `{:?}` on a raw
pointer and a hand-written `0x{:x}` — is only ever the second spelling.

Every address-bearing repr was therefore wrong on Windows. Measured against
CPython 3.14 on the same host, fifteen kinds disagreed: function, object,
generator, coroutine, async_generator, bound and built-in method,
method-wrapper, cell, weakref, memoryview, ContextVar, Token, code, frame and
both `_thread` locks. They now go through one `display::repr_addr`, and the
shapes are identical. `_pickle`'s cyclic-object message names an address the
same way and is the one such site that is not a repr.

`surrogate_name_messages` asserted the glibc spelling against `id()`, so it was
the REFERENCE that failed it on every Windows runner — a script that fails on
CPython measures nothing, and the backends were being compared against a failing
oracle. It builds the platform's spelling now.

The frame repr's `file '...'` is left raw: `pyframe.py:849-853` interpolates
`'%s'`, and CPython's `%R` there escapes the backslashes a Windows path is full
of. That is a parity-source disagreement rather than this fix's business.
`bridges_compiled 102 -> 5` and `guard_failures 20497 -> 1011`, byte-identical
on dynasm, cranelift and wasm.

The recorded 102/20497 are the counters the `Cls.__name__` fold produced while
its metaclass guard could not be discharged — it read the raw `w_class` slot
where the fast path answers through the `gettypefor(ob_type)` fallback, so a
receiver reached through that fallback got a `guard_value(NULL, type)` that
failed on every execution. #1106 narrowed the fold; the baseline still names
what the defect measured.

Read back rather than predicted: ubuntu-24.04, macos-latest and windows-latest
all report 5/1011 on main, and so does a local windows run. `retraces_compiled`
joins the file because the recorder now writes it; it is 0, which is what the
comparison already read for its absence.
…nd two NTFS-only assertions

`sys.getwindowsversion` built its structseq type inside the call, so every
answer was an instance of a different class and
`type(sys.getwindowsversion()) is type(sys.getwindowsversion())` read False
where CPython reads True. It was the one of pyre's ten structseq types that did
not already cache its type in a `OnceLock` — `stat_result`, `terminal_size`,
`uname_result`, `statvfs_result`, `waitid_result`, `times_result`,
`struct_time`, `_ExceptHookArgs` and `UnraisableHookArgs` all do — and it now
does the same. Measured: a probe over every structseq both runtimes carry
reports the types equal for all nine others and for all of CPython's, so this
constructor was the whole divergence. That the cache is a bare `usize` the GC
cannot see is the established pattern rather than a new bet, and it holds: 200
`os.stat` answers dropped across forced collections plus 60MB of churn leave the
cached type identical and its fields readable.

`structseq_type_identity` pins the property for every structseq the host
carries, and fails on the binary built before this commit naming exactly
`sys.getwindowsversion`.

The parity runner decoded its child's stderr on the normal path only.
`subprocess.run(text=True)` decodes what `communicate()` returns; the timeout
path raises with the raw chunks it had joined, which is bytes on POSIX and str
on Windows. A timed-out script would have reported one `b'...\n...'` line — the
unreadable shape the rest of this branch exists to remove.

Two assertions in `os_utime_pathconf_truncate` could only ever have held on
NTFS. The step that runs them has not reached ubuntu or macos: `check.py` runs
first in that job and has been failing, so the parity suite never starts there.

* `ns=(2**62, 2**62)` read back `4611686018427387900`, which is 2**62 rounded
  down to a FILETIME's 100ns tick. ext4 and APFS keep the nanosecond and answer
  `...904`. Both spellings now come from one `storable()`, which also replaces
  the `-1`/`-100` conditional above it.
* `utime(p, (8.8e11, 8.8e11))` names the year 29880, which no filesystem but
  NTFS reaches — an APFS timestamp is itself an int64 of nanoseconds, so 2262
  bounds it too, and ext4 stores a 34-bit second and stops in 2446. What every
  platform is held to is the identity the `i128` widening buys,
  `st_mtime_ns == int(st_mtime) * 1_000_000_000`, wherever the write succeeds;
  the exact value is asserted only where the second survived the round trip.

The remaining review note asked for f-strings in `surrogate_name_messages`.
Left alone: the file builds every expected repr with `%` formatting, including
the assertions this branch did not touch, and nothing lints for it.
…ut run, and decode the report

`classify` recorded `last_stderr_line` for a FAIL. This runner sets
`MAJIT_STATS`, so the last thing every process writes is the JIT summary:
every FAIL in the suite recorded `rc=1 Compilation time: <n>ms`, a line that
names no test. The nightly report carries 118 of them, all the same. A FAIL
now carries unittest's own account instead — the closing `FAILED (...)` and
the `FAIL:`/`ERROR:` case headers, up to four of them and a count of the rest.
An IMPORTERROR never reached unittest, so it keeps the tail.

`TimeoutExpired` was caught and discarded, leaving `timeout 120s`. It carries
the output the child had produced, which for a unittest module is the progress
dots — the record of which case it stopped in. `text=True` decodes what
`communicate()` returns, not what the timeout path raises, so the partial
arrives as bytes on POSIX and str on Windows and both are accepted.

The report's box-drawing killed the run with a `UnicodeEncodeError` on a
console whose codepage cannot spell it, before any test ran; stdout is
reconfigured the way the parity runner's is.
…tlocale that reaches the CRT

The `LC_*` values came from libc under `cfg(unix)` and from a hardcoded POSIX
table everywhere else, so Windows published `LC_CTYPE=0, LC_ALL=6` where the
MSVC CRT numbers them `LC_ALL=0, LC_COLLATE=1, LC_CTYPE=2`. Every constant
named a different category than the one it was passed to. They now come from
libc there too, which carries the CRT's own numbering.

`LC_MESSAGES` is a POSIX category the MSVC CRT has no counterpart for, and
CPython does not define it on Windows; it is registered under `cfg(unix)`.

`setlocale` was gated on `all(unix, host_env)`, so on Windows it fell to the
no-libc arm and answered "C" for every name it was handed, reporting success
for a locale that was not installed. `host_env::locale::setlocale` is not
unix-only — it calls `libc::setlocale`, which Windows has. The gate now admits
Windows, and an uninstallable name raises `locale.Error` as CPython's does.

Measured against CPython 3.14 on Windows: the six category numbers agree,
neither has `LC_MESSAGES`, `en_US.iso88591` raises `locale.Error` on both and
`en_US.utf8` and `English_United States.1252` are accepted by both.
`locale_categories` pins all three properties and fails on the prior binary.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pyre/pyre-interpreter/src/module/_locale/interp_locale.rs (1)

297-313: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep Windows locale readers consistent with setlocale.

This window now changes the Windows host locale, but localeconv, strcoll, and strxfrm still use Unix-only host-backed branches. After a Windows caller sets LC_NUMERIC or LC_COLLATE, these APIs still return fixed C-locale or bytewise results. Extend the host-backed reader paths to Windows, or keep Windows locale state fixed, and verify rustpython_host_env::locale::localeconv_data, strcoll, and strxfrm support Windows before changing cfg guards.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/module/_locale/interp_locale.rs` around lines 297 -
313, Keep Windows behavior consistent across locale APIs: either extend the
host-backed branches for localeconv_data, strcoll, and strxfrm to Windows after
confirming rustpython_host_env::locale supports them, or constrain setlocale’s
host-backed path so Windows locale state remains fixed. Ensure Windows calls to
localeconv, strcoll, and strxfrm reflect the same state established by setlocale
rather than falling back to Unix-only or bytewise behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/pyre-interpreter/src/module/sys/vm.rs`:
- Around line 1377-1393: Replace the process-global OnceLock<usize> in the
getwindowsversion path with the existing interpreter-owned structseq state used
by stat_result_seq_type. Store the created type as a traced GC root, integrating
it with pin_root and walk_gc_refs as appropriate, and retrieve it through that
state before new_instance_with_extra. Do not retain the PyObjectRef as an
untraced static pointer.

---

Outside diff comments:
In `@pyre/pyre-interpreter/src/module/_locale/interp_locale.rs`:
- Around line 297-313: Keep Windows behavior consistent across locale APIs:
either extend the host-backed branches for localeconv_data, strcoll, and strxfrm
to Windows after confirming rustpython_host_env::locale supports them, or
constrain setlocale’s host-backed path so Windows locale state remains fixed.
Ensure Windows calls to localeconv, strcoll, and strxfrm reflect the same state
established by setlocale rather than falling back to Unix-only or bytewise
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 60e5f7c4-d874-4484-a190-31a9738c4fa1

📥 Commits

Reviewing files that changed from the base of the PR and between 93eb41e and 0f0b915.

📒 Files selected for processing (10)
  • pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats
  • pyre/bench/synth/recursion_memo_branch.wasm.jitstats
  • pyre/bench/synth/wasm_ca_trampoline_decline.wasm.jitstats
  • pyre/cpython_tests/run.py
  • pyre/extra_tests/parity_tests/locale_categories.py
  • pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
  • pyre/extra_tests/parity_tests/run.py
  • pyre/extra_tests/parity_tests/structseq_type_identity.py
  • pyre/pyre-interpreter/src/module/_locale/interp_locale.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs

Comment on lines +1377 to +1393
// Built once, like [`stat_result_seq_type`]: the type is the
// answer's identity, so making a fresh one per call would leave
// `type(sys.getwindowsversion())` a different class each time.
static SEQ_TYPE: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
let cls = *SEQ_TYPE.get_or_init(|| {
crate::_structseq::make_struct_seq_with_extra(
"sys.getwindowsversion",
&["major", "minor", "build", "platform", "service_pack"],
&[
"service_pack_major",
"service_pack_minor",
"suite_mask",
"product_type",
"platform_version",
],
) as usize
}) as PyObjectRef;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Store the cached structseq type in GC-aware interpreter state.

SEQ_TYPE stores a PyObjectRef as usize in a process-global OnceLock. The collector cannot trace or forward this value. A moving collection can leave cls stale before new_instance_with_extra uses it.

Create the type in the existing interpreter-owned structseq state and preserve it as a traced root. Do not keep the object pointer in an untraced static.

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 6 \
  'stat_result_seq_type|make_struct_seq_with_extra|OnceLock<usize>|pin_root|walk_gc_refs' \
  pyre/pyre-interpreter/src

As per coding guidelines: “Before choosing a Rust collection or thread-local, locate the corresponding RPython/PyPy owner and preserve its storage shape and semantics.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/module/sys/vm.rs` around lines 1377 - 1393, Replace
the process-global OnceLock<usize> in the getwindowsversion path with the
existing interpreter-owned structseq state used by stat_result_seq_type. Store
the created type as a traced GC root, integrating it with pin_root and
walk_gc_refs as appropriate, and retrieve it through that state before
new_instance_with_extra. Do not retain the PyObjectRef as an untraced static
pointer.

Source: Coding guidelines

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f0b915ec1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

pub(crate) fn repr_addr(addr: usize) -> String {
if cfg!(windows) {
format!("0x{addr:0width$X}", width = size_of::<usize>() * 2)
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve PyPy's lowercase address spelling

On Windows, this uppercase X formatter makes every affected repr diverge from PyPy. PyPy's W_Root.getaddrstring() explicitly emits a fixed-width sequence using lowercase af on every platform, rather than delegating to the CRT %p; therefore Windows function, generator, frame, code, object, and related reprs now use the wrong observable spelling. Use fixed-width lowercase hexadecimal to retain the upstream representation.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

("st_atime_ns", pyre_object::w_int_new(st_atime_ns)),
("st_mtime_ns", pyre_object::w_int_new(st_mtime_ns)),
("st_ctime_ns", pyre_object::w_int_new(st_ctime_ns)),
("st_atime_ns", w_time_ns(st_atime_ns)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Root wide timestamps before allocating the next field

For a file dated after 2262, w_time_ns(st_atime_ns) creates a GC-managed Python long, but it is stored only in the unrooted extras vector while the following w_time_ns calls allocate additional bigint payloads and may collect. new_instance_with_extra does not pin these values until after the entire argument vector has been evaluated, so under collection pressure the first long can be reclaimed and the eventual structseq receives a stale pointer. Pin each wide value while constructing the vector, or otherwise root the complete set before any subsequent allocation.

Useful? React with 👍 / 👎.

#1106 `4555e3d76b` dropped the wasm32 arm of the self-recursive root-bridge
inline and recorded what that moved: `ca_bridge_multiframe_resume_double_call`
2581 -> 2592, `wasm_ca_trampoline_decline` 404 -> 601 and
`recursion_memo_branch` 4724 -> 4704. #1086 `e5eff81684` wrote all three back
to their pre-#1106 values, and ubuntu has failed on them every run since.

#1086 resizes 130 fixtures so pypy's side clears the measurement floor, and
re-recording a resized fixture's counters is part of that. It moves 34 baseline
values; 26 sit in fixtures whose `.py` it also changed. These three — and
`pypy_type_surface`, restored in 958f66c — are among the eight it moved with
no workload change beside them, which is a snapshot taken on a base predating
#1106.

Measured rather than reverted: a local wasm run reads 2592, 601 and 4704, which
are #1106's figures and the ones ubuntu observes. The remaining two of those
eight, `closure_per_call` 418 and `recursive_call_frame_relocation` 638, read
what they already record and are left alone.

The lower counts are not the better state. #1106 measured removing the decline
at -20.1%/-20.7% wasm CPU on `wasm_ca_trampoline_decline`; the +197 guard
failures buy that. `ca_bridge_multiframe_resume_double_call` pays +1.2%/+2.7%
and in exchange reports the 16 bridges / 0 aborts the dynasm baseline records,
where the decline left wasm at 16/1.

`retraces_compiled=0` comes with the re-record: these were the only wasm
baselines missing the key, which `_parse_jit_stats` was defaulting.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24807b7794

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1410 to +1414
pyre_object::w_tuple_new(vec![
w_int_new(info.major as i64),
w_int_new(info.minor as i64),
w_int_new(info.build as i64),
]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Leave platform_version empty to match PyPy

On Windows, this publishes (major, minor, build) as platform_version, but the corresponding implementation in pypy/module/sys/vm.py:280-298 deliberately stores None there so platform.py uses the five primary GetVersionEx fields. Code that checks sys.getwindowsversion().platform_version therefore observes a pyre-only tuple; preserve the upstream empty value rather than introducing alternate CPython-style semantics.

AGENTS.md reference: AGENTS.md:L231-L232

Useful? React with 👍 / 👎.

@youknowone
youknowone merged commit eba36d1 into main Aug 9, 2026
15 of 17 checks passed
@youknowone
youknowone deleted the win-work branch August 9, 2026 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant