diff --git a/pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats b/pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats index 943439afa35..2f53bb8072f 100644 --- a/pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats +++ b/pyre/bench/synth/ca_bridge_multiframe_resume_double_call.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=2581 +guard_failures=2592 internal_compile_panics=0 loops_aborted=0 loops_compiled=3 +retraces_compiled=0 diff --git a/pyre/bench/synth/pypy_type_surface.cranelift.jitstats b/pyre/bench/synth/pypy_type_surface.cranelift.jitstats index d6fdf92605f..81c8187a9e3 100644 --- a/pyre/bench/synth/pypy_type_surface.cranelift.jitstats +++ b/pyre/bench/synth/pypy_type_surface.cranelift.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=102 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=20497 +guard_failures=1011 internal_compile_panics=0 loops_aborted=0 loops_compiled=11 +retraces_compiled=0 diff --git a/pyre/bench/synth/pypy_type_surface.dynasm.jitstats b/pyre/bench/synth/pypy_type_surface.dynasm.jitstats index d6fdf92605f..81c8187a9e3 100644 --- a/pyre/bench/synth/pypy_type_surface.dynasm.jitstats +++ b/pyre/bench/synth/pypy_type_surface.dynasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=102 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=20497 +guard_failures=1011 internal_compile_panics=0 loops_aborted=0 loops_compiled=11 +retraces_compiled=0 diff --git a/pyre/bench/synth/pypy_type_surface.wasm.jitstats b/pyre/bench/synth/pypy_type_surface.wasm.jitstats index d6fdf92605f..81c8187a9e3 100644 --- a/pyre/bench/synth/pypy_type_surface.wasm.jitstats +++ b/pyre/bench/synth/pypy_type_surface.wasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=102 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=20497 +guard_failures=1011 internal_compile_panics=0 loops_aborted=0 loops_compiled=11 +retraces_compiled=0 diff --git a/pyre/bench/synth/recursion_memo_branch.wasm.jitstats b/pyre/bench/synth/recursion_memo_branch.wasm.jitstats index cce369775d9..3e2283230f5 100644 --- a/pyre/bench/synth/recursion_memo_branch.wasm.jitstats +++ b/pyre/bench/synth/recursion_memo_branch.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=4724 +guard_failures=4704 internal_compile_panics=0 loops_aborted=1 loops_compiled=3 +retraces_compiled=0 diff --git a/pyre/bench/synth/wasm_ca_trampoline_decline.wasm.jitstats b/pyre/bench/synth/wasm_ca_trampoline_decline.wasm.jitstats index 2af84b441ef..1b618851f7b 100644 --- a/pyre/bench/synth/wasm_ca_trampoline_decline.wasm.jitstats +++ b/pyre/bench/synth/wasm_ca_trampoline_decline.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=404 +guard_failures=601 internal_compile_panics=0 loops_aborted=1 loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/cpython_tests/run.py b/pyre/cpython_tests/run.py index ada9b5f5bbe..1d0031e099c 100644 --- a/pyre/cpython_tests/run.py +++ b/pyre/cpython_tests/run.py @@ -156,6 +156,34 @@ def last_stderr_line(err: str) -> str: return "" +def failure_digest(out: str, err: str) -> str: + """Which cases unittest reported against, and its closing verdict. + + A FAIL's tail is not its cause: this runner sets `MAJIT_STATS`, so the last + thing every process writes is the JIT summary, and `last_stderr_line` reads + back `Compilation time: 8.1ms` for a module whose real answer is two named + assertions several hundred lines above it. Every FAIL in the suite recorded + that same line, which named no test at all. + + unittest writes `FAIL: ` / `ERROR: ` headers and one closing + `FAILED (...)`; those are the runner's own account of what went wrong, and + they are what a CI log needs to carry. + """ + lines = f"{out}\n{err}".splitlines() + cases: list[str] = [] + verdict = "" + for line in lines: + line = line.strip() + if line.startswith(("FAIL: ", "ERROR: ")) and line not in cases: + cases.append(line) + elif line.startswith("FAILED ("): + verdict = line + shown = cases[:4] + if len(cases) > len(shown): + shown.append(f"(+{len(cases) - len(shown)} more)") + return " | ".join(part for part in (verdict, *shown) if part) + + def death_signal(rc: int) -> str: """`SIGBUS` for a run killed by a signal, else a bare return code. @@ -199,8 +227,11 @@ def classify(rc: int, out: str, err: str) -> tuple[str, str]: # skips it too, so it is honestly SKIP, not an interpreter gap. if not ran and "SkipTest" in err: return "SKIP", f"rc={rc} {last}"[:120] - status = "FAIL" if ran else "IMPORTERROR" - return status, f"rc={rc} {last}"[:120] + if ran: + # An IMPORTERROR never reached unittest, so its tail is all there is; + # a FAIL has unittest's own account, and only that names a test. + return "FAIL", f"rc={rc} {failure_digest(out, err) or last}"[:300] + return "IMPORTERROR", f"rc={rc} {last}"[:120] # ── discovery ──────────────────────────────────────────────────────── @@ -324,8 +355,17 @@ def run_module(binary: Path, module: str, mode: str, timeout: int, cmd, cwd=cwd, env=module_env, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, ) - except subprocess.TimeoutExpired: - return "TIMEOUT", f"timeout {timeout}s" + except subprocess.TimeoutExpired as expired: + # A bare "timeout 120s" says only that the module is slow, never + # which case it stopped in — and unittest's progress dots, the one + # record of how far it got, were being thrown away here. `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, so both spellings have to be accepted. + partial = expired.stderr or "" + if isinstance(partial, bytes): + partial = partial.decode("utf-8", "replace") + return "TIMEOUT", f"timeout {timeout}s {last_stderr_line(partial)}"[:300] return classify(proc.returncode, proc.stdout or "", proc.stderr or "") @@ -390,6 +430,12 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() + # The report is box-drawn and the module names it echoes come from the + # stdlib, so a console whose codepage cannot spell one of those characters + # used to kill the run with a `UnicodeEncodeError` before it reached a + # single test — the header alone does it on cp949. + sys.stdout.reconfigure(encoding="utf-8", errors="replace", line_buffering=True) + binary = Path(args.binary) if args.binary else TARGET_RELEASE / f"{BIN_NAME[args.backend]}{EXE}" binary = binary.resolve() if not args.list and not binary.exists(): diff --git a/pyre/extra_tests/parity_tests/frame_clear_finalization.py b/pyre/extra_tests/parity_tests/frame_clear_finalization.py index 54ac1910175..9e71ecd6f16 100644 --- a/pyre/extra_tests/parity_tests/frame_clear_finalization.py +++ b/pyre/extra_tests/parity_tests/frame_clear_finalization.py @@ -1,6 +1,6 @@ import gc -import resource import sys +import time import traceback import weakref @@ -124,11 +124,14 @@ def raises(): check = Check() count = 200 - before = resource.getrusage(resource.RUSAGE_SELF) + # `process_time` rather than `resource.getrusage`: the CPU this process has + # spent is the measurement, and `resource` is a POSIX module, so importing + # it would take the five checks above off Windows along with this one. + before = time.process_time() for _ in range(count): check.assertRaises(Expected, raises) - after = resource.getrusage(resource.RUSAGE_SELF) - per_op_ms = (after.ru_utime - before.ru_utime) * 1000.0 / count + after = time.process_time() + per_op_ms = (after - before) * 1000.0 / count assert per_op_ms < 1.0, per_op_ms diff --git a/pyre/extra_tests/parity_tests/locale_categories.py b/pyre/extra_tests/parity_tests/locale_categories.py new file mode 100644 index 00000000000..167a7116ecd --- /dev/null +++ b/pyre/extra_tests/parity_tests/locale_categories.py @@ -0,0 +1,85 @@ +"""Which locale categories a platform has, and a setlocale that admits failure. + +- The `LC_*` numbers belong to the host's C library, and POSIX and the MSVC CRT + number them differently — `LC_ALL` is 6 on one and 0 on the other. A table of + POSIX values published on Windows does not merely read wrong: every number + names a *different* category there, so setting one sets another. +- `LC_MESSAGES` is a POSIX category the MSVC CRT has no counterpart for, and + `locale.py` asks whether the name is here before using it. +- `setlocale` answered "C" for every name it was given wherever the real one + was not being called, so a locale the host cannot install looked installed. + `locale.Error` is how a caller finds out it is not — and code that asks for a + locale in order to skip when it is missing gets the skip wrong otherwise. +""" + +import locale +import sys + + +def check(cond, what): + if not cond: + raise AssertionError(what) + + +def raises(call, exc): + try: + call() + except exc as e: + return e + raise AssertionError(f"{exc.__name__} was not raised") + + +CATEGORIES = ["LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", + "LC_TIME"] + +# ── which categories exist, and what they are numbered ──────────────────── +for name in CATEGORIES: + check(hasattr(locale, name), f"locale has no {name}") + +values = [getattr(locale, name) for name in CATEGORIES] +check(len(set(values)) == len(values), f"two categories share a number: {values}") + +# `LC_MESSAGES` is the one category whose presence is the platform's answer +# rather than a constant, so it is asserted as presence and not as a number. +check( + hasattr(locale, "LC_MESSAGES") == (sys.platform != "win32"), + f"LC_MESSAGES presence is wrong for {sys.platform}", +) + +if sys.platform == "win32": + # The MSVC CRT's own numbering, which is not POSIX's in any position. + check(locale.LC_ALL == 0, locale.LC_ALL) + check(locale.LC_COLLATE == 1, locale.LC_COLLATE) + check(locale.LC_CTYPE == 2, locale.LC_CTYPE) + check(locale.LC_MONETARY == 3, locale.LC_MONETARY) + check(locale.LC_NUMERIC == 4, locale.LC_NUMERIC) + check(locale.LC_TIME == 5, locale.LC_TIME) + +# ── setlocale answers about the locale that was installed ───────────────── +# The one-argument form asks rather than sets, and what it answers has to be a +# name the two-argument form accepts back — the round trip is how `addCleanup` +# in a test, or a library restoring what it borrowed, puts the locale back. +for name in CATEGORIES: + category = getattr(locale, name) + current = locale.setlocale(category) + check(isinstance(current, str) and current, f"setlocale({name}) -> {current!r}") + check(locale.setlocale(category, current) == current, f"{name} did not round trip") + +# "C" is the one locale every host has. +check(locale.setlocale(locale.LC_ALL, "C") == "C", "LC_ALL could not be set to C") + +# A name no host can install is refused. Answering "C" here — the shape of a +# setlocale that never reached the C library — reports success for a locale +# that was not installed, and the caller then runs the work it meant to skip. +for absent in ("no-such-locale-xyz", "xx_YY.INVALID"): + raises(lambda: locale.setlocale(locale.LC_CTYPE, absent), locale.Error) + check( + locale.setlocale(locale.LC_CTYPE) == "C", + "a refused setlocale changed the installed locale", + ) + +# An embedded NUL cannot reach a C string, and is a value error rather than a +# locale that could not be found. +raises(lambda: locale.setlocale(locale.LC_CTYPE, "C\0C"), ValueError) + +print("OK") diff --git a/pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py b/pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py index 4f68ea9cccd..eb601259735 100644 --- a/pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py +++ b/pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py @@ -29,6 +29,19 @@ def raises(call, exc): raise AssertionError(f"{exc.__name__} was not raised") +# A Windows FILETIME counts 100ns ticks, so a nanosecond that is not a multiple +# of 100 is not a time that filesystem can hold; ext4 and APFS both store the +# nanosecond itself. The negative range is not what differs — only how finely +# it is kept — so the checks below round to what the host can hold rather than +# skipping a platform. +GRANULARITY_NS = 100 if sys.platform == "win32" else 1 + + +def storable(ns): + """`ns` as the filesystem under this test reads it back.""" + return ns // GRANULARITY_NS * GRANULARITY_NS + + d = tempfile.mkdtemp() atexit.register(shutil.rmtree, d, ignore_errors=True) p = os.path.join(d, "f") @@ -39,48 +52,85 @@ def raises(call, exc): # The seconds are the floor of the value and the nanoseconds are what is left # above it, so -1ns is the last nanosecond of 1969 rather than a value with no # representation. -# -# Windows is left out of this block: a FILETIME counts 100ns ticks, so -# ns=(-1, -1) is not a time that filesystem can hold (it reads -1 back as -# -100), and the descriptor form below is not one Windows advertises. The -# keyword `times` case after the block, which lands on whole seconds, is -# written through SetFileTime everywhere. -if sys.platform != "win32": - os.utime(p, ns=(-1, -1)) - check(os.stat(p).st_mtime_ns == -1, f"utime(ns=(-1,-1)) -> {os.stat(p).st_mtime_ns}") - os.utime(p, ns=(-2_500_000_000, -2_500_000_000)) - check(os.stat(p).st_mtime_ns == -2_500_000_000, "utime(ns) lost a negative second") - - os.utime(p, (-1.5, -2.5)) - check( - os.stat(p).st_mtime_ns == -2_500_000_000, - f"utime((-1.5,-2.5)) -> {os.stat(p).st_mtime_ns}", - ) - check(os.stat(p).st_atime_ns == -1_500_000_000, "utime(times) lost the access time") - - # The same through a descriptor, which is the form supports_fd advertises. - if os.utime in os.supports_fd: - fd = os.open(p, os.O_RDWR) - try: - os.utime(fd, ns=(-3_000_000_000, -4_000_000_000)) - check( - os.stat(p).st_mtime_ns == -4_000_000_000, - "utime(fd, ns) lost a negative second", - ) - finally: - os.close(fd) +os.utime(p, ns=(-1, -1)) +check( + os.stat(p).st_mtime_ns == storable(-1), + f"utime(ns=(-1,-1)) -> {os.stat(p).st_mtime_ns}", +) +os.utime(p, ns=(-2_500_000_000, -2_500_000_000)) +check(os.stat(p).st_mtime_ns == -2_500_000_000, "utime(ns) lost a negative second") -# `times` is the one argument here that may be spelled either way — it sits -# before the keyword-only marker. The pair is one the platform holds: Windows -# refuses times before the epoch, for the reason given above. -atime, mtime = (5.0, 6.0) if sys.platform == "win32" else (-5.0, -6.0) -os.utime(p, times=(atime, mtime)) +os.utime(p, (-1.5, -2.5)) check( - os.stat(p).st_mtime_ns == int(mtime) * 1_000_000_000, - f"utime(times=...) by keyword -> {os.stat(p).st_mtime_ns}", + os.stat(p).st_mtime_ns == -2_500_000_000, + f"utime((-1.5,-2.5)) -> {os.stat(p).st_mtime_ns}", ) +check(os.stat(p).st_atime_ns == -1_500_000_000, "utime(times) lost the access time") + +# The same through a descriptor, which is the form supports_fd advertises. +if os.utime in os.supports_fd: + fd = os.open(p, os.O_RDWR) + try: + os.utime(fd, ns=(-3_000_000_000, -4_000_000_000)) + check( + os.stat(p).st_mtime_ns == -4_000_000_000, + "utime(fd, ns) lost a negative second", + ) + finally: + os.close(fd) + +# `times` is the one argument here that may be spelled either way — it sits +# before the keyword-only marker. +os.utime(p, times=(-5.0, -6.0)) +check(os.stat(p).st_mtime_ns == -6_000_000_000, "utime(times=...) by keyword") raises(lambda: os.utime(p, (1, 2), times=(3, 4)), TypeError) +# ── what a second may be spelled as ─────────────────────────────────────── +# A value that is not a number is refused by type rather than by a failed +# conversion, one no `time_t` can hold by overflow rather than by value, and +# a NaN has no floor to take at all. +e = raises(lambda: os.utime(p, ("a", "b")), TypeError) +check(str(e) == "argument must be int or float, not str", str(e)) +e = raises(lambda: os.utime(p, (None, 1)), TypeError) +check(str(e) == "argument must be int or float, not NoneType", str(e)) +for far in (1e30, float("inf"), 2**200, -(2**200)): + e = raises(lambda: os.utime(p, (far, 0)), OverflowError) + check(str(e) == "timestamp out of range for platform time_t", f"{far!r}: {e}") +e = raises(lambda: os.utime(p, (float("nan"), 0.0)), ValueError) +check(str(e) == "Invalid value NaN (not a number)", str(e)) +# `times` is the argument that also has a `None` spelling, and its message +# names it; `ns` has no such form. +e = raises(lambda: os.utime(p, (1,)), TypeError) +check(str(e) == "utime: 'times' must be either a tuple of two ints or None", str(e)) +e = raises(lambda: os.utime(p, ns=(1,)), TypeError) +check(str(e) == "utime: 'ns' must be a tuple of two ints", str(e)) + +# `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 == storable(2**62), os.stat(p).st_mtime_ns) + +# 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. +# +# Only a filesystem that reaches such a date can be asked about it, and by +# construction that cannot be every one: an APFS timestamp IS an int64 of +# nanoseconds, so 2262 is its ceiling too, and ext4 stores a 34-bit second and +# stops in 2446. Both clamp what they are handed (some hosts refuse it), where +# NTFS counts 100ns ticks to the year 30828. So the identity — the seconds in +# nanoseconds, whatever second was stored — is what every platform is held to, +# and the exact value only where the second survived the round trip. +FAR = 8.8e11 +try: + os.utime(p, (FAR, FAR)) +except OSError: + pass +else: + st = os.stat(p) + check(st.st_mtime_ns == int(st.st_mtime) * 1_000_000_000, st.st_mtime_ns) + if st.st_mtime == FAR: + check(st.st_mtime_ns == 880_000_000_000_000_000_000, st.st_mtime_ns) + # Back to a time the rest of the file can be reasoned about. os.utime(p, ns=(1_000_000_000, 2_000_000_000)) @@ -89,20 +139,26 @@ def raises(call, exc): # question — the terminal-only limits are not ones a regular file has. What no # answer may be is None: a host with no determinate value says so with -1. # -# pathconf and pathconf_names are POSIX-only; Windows has neither, so the -# section is skipped there rather than asked of a name that cannot answer. -if sys.platform != "win32": +# `pathconf` and the `pathconf_names` table it resolves through are a POSIX +# surface; neither runtime carries them on Windows, so there is nothing to +# compare there. `hasattr` rather than a platform list: what the section needs +# is the name, not the platform. +def limits(target): + for name in sorted(os.pathconf_names): + try: + limit = os.pathconf(target, name) + except OSError: + continue + check(isinstance(limit, int), f"pathconf({name!r}) answered {limit!r}") + check(limit >= -1, f"pathconf({name!r}) answered {limit}") + yield name, limit - def limits(target): - for name in sorted(os.pathconf_names): - try: - limit = os.pathconf(target, name) - except OSError: - continue - check(isinstance(limit, int), f"pathconf({name!r}) answered {limit!r}") - check(limit >= -1, f"pathconf({name!r}) answered {limit}") - yield name, limit +check( + hasattr(os, "pathconf") == hasattr(os, "pathconf_names"), + "one of pathconf / pathconf_names is here without the other", +) +if hasattr(os, "pathconf"): answered = dict(limits(p)) check(answered, "pathconf answered no name at all") check("PC_NAME_MAX" in answered, "pathconf refused PC_NAME_MAX on a regular file") diff --git a/pyre/extra_tests/parity_tests/run.py b/pyre/extra_tests/parity_tests/run.py index 4167f2744ee..c9caba37676 100644 --- a/pyre/extra_tests/parity_tests/run.py +++ b/pyre/extra_tests/parity_tests/run.py @@ -48,6 +48,7 @@ import subprocess import sys from pathlib import Path +from typing import NamedTuple HERE = Path(__file__).resolve().parent ROOT = HERE.parent.parent.parent @@ -111,7 +112,30 @@ def _script_env(script: Path) -> dict[str, str]: return {m.group(1): m.group(2) for m in _ENV_DIRECTIVE.finditer(text)} -def _run(cmd: list[str], script: Path, env: dict[str, str] | None) -> tuple[bool, str]: +TIMEOUT = 30 + + +class Failure(NamedTuple): + """One (script, runner) pair that did not pass, and the evidence.""" + + script: str + backend: str + # What the runner concluded, in one line — fits beside the table row. + reason: str + # The child's stderr, verbatim. A traceback is the answer to "why", and it + # only reads as one when its own line breaks survive. + stderr: str + + +def _run( + cmd: list[str], script: Path, env: dict[str, str] | None +) -> tuple[bool, str, str]: + """Whether the script passed, why it did not, and the child's stderr. + + The reason is kept apart from the stderr rather than formatted into it: a + `repr` of a whole traceback is one unreadable line, and the run wants the + short verdict beside the row and the traceback in the report at the end. + """ try: proc = subprocess.run( cmd + [str(script)], @@ -119,27 +143,32 @@ def _run(cmd: list[str], script: Path, env: dict[str, str] | None) -> tuple[bool text=True, encoding="utf-8", errors="replace", - timeout=30, + timeout=TIMEOUT, env=None if env is None else {**os.environ, **env}, ) - except subprocess.TimeoutExpired: - return False, "timeout" - out = proc.stdout - err = proc.stderr - lines = [line for line in out.splitlines() if line.strip()] + except subprocess.TimeoutExpired as expired: + # `text=True` decodes what `communicate()` RETURNS; the timeout path + # raises with the raw chunks it had joined so far, so what arrives here + # is bytes on POSIX and str on Windows. Reporting the bytes would print + # one `b'...\n...'` line, which is the unreadable shape this report + # exists to avoid. + partial = expired.stderr or "" + if isinstance(partial, bytes): + partial = partial.decode("utf-8", "replace") + return False, f"timed out after {TIMEOUT}s", partial + lines = [line for line in proc.stdout.splitlines() if line.strip()] last = lines[-1] if lines else "" - ok = proc.returncode == 0 and last == "OK" - if ok: - detail = "" - elif proc.returncode == 0: + if proc.returncode == 0 and last == "OK": + return True, "", "" + if proc.returncode == 0: # The script ran to completion and never announced itself. Reporting # this as `rc=0 last=''` reads like an interpreter that produced # nothing, and it is reported once per runner, so three of them make a # sound fixture that forgot its last line look like a pyre failure. - detail = f"exited 0 without a final 'OK' line (last non-empty line {last!r})" + reason = f"exited 0 without a final 'OK' line (last non-empty line {last!r})" else: - detail = f"rc={proc.returncode} last={last!r} stderr={err.strip()!r}" - return ok, detail + reason = f"exited {proc.returncode} (last non-empty stdout line {last!r})" + return False, reason, proc.stderr PROBE = "import sys; print(sys.version_info[0], sys.version_info[1]); print(sys.executable)" @@ -206,6 +235,45 @@ def _cpython() -> str: ) +def _report(failures: list[Failure]) -> None: + """The evidence for every failure, printed last and on stdout. + + Last because a CI log is read from its end, and the table above it is two + hundred rows: a reader who has to scroll up for the reason reruns the job + instead. On stdout because the table is — these lines used to go to stderr, + which is unbuffered where a piped stdout 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. + """ + print("=" * 72) + print(f"{len(failures)} failure(s)") + for failure in failures: + print() + print(f" {failure.script} [{failure.backend}]: {failure.reason}") + for line in failure.stderr.strip().splitlines(): + print(f" {line}") + print("=" * 72) + + +def _annotate(failures: list[Failure]) -> None: + """One GitHub Actions error annotation per failure. + + An annotation shows on the pull request and the job summary, so the + reason survives even for a reader who never opens the log. The message is + a single line by the format's own rule, so it carries the verdict and the + exception line rather than the whole traceback. + """ + for failure in failures: + spoken = [line.strip() for line in failure.stderr.splitlines() if line.strip()] + tail = spoken[-1] if spoken else "" + message = f"{failure.backend}: {failure.reason}" + if tail: + message += f" | {tail}" + escaped = message.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A") + path = (HERE / failure.script).relative_to(ROOT).as_posix() + print(f"::error file={path},title=parity::{escaped}") + + def _runners( only_dynasm: bool, only_cranelift: bool, gc_poison: bool ) -> list[tuple[str, list[str], dict[str, str] | None]]: @@ -228,6 +296,15 @@ def main() -> int: parser.add_argument("--gc-poison", action="store_true") args = parser.parse_args() + # A piped stdout is block-buffered, so without line buffering the rows + # arrive in the log in one burst at the end and a run that dies mid-way + # names none of the scripts it got through. The encoding is pinned because + # the report echoes a child's stderr, and several of these scripts are + # about names no console codepage can spell — printing one of those on a + # non-UTF-8 console used to kill the runner with a `UnicodeEncodeError` + # instead of reporting the failure it was in the middle of explaining. + sys.stdout.reconfigure(encoding="utf-8", errors="replace", line_buffering=True) + runners = _runners(args.dynasm_only, args.cranelift_only, args.gc_poison) scripts, skipped = _scripts() if not scripts: @@ -240,27 +317,31 @@ def main() -> int: print(f"skipped ({sys.platform} not in its platforms): {script.name}") print() - fail = 0 + failures: list[Failure] = [] for script in scripts: name = script.name pinned = _script_env(script) row: list[str] = [f" {name:<36s}"] + reasons: list[str] = [] for backend, cmd, env in runners: merged = {**(env or {}), **pinned} - ok, detail = _run(cmd, script, merged or None) - mark = "OK" if ok else "FAIL" - row.append(f"{backend}={mark}") + ok, reason, err = _run(cmd, script, merged or None) + row.append(f"{backend}={'OK' if ok else 'FAIL'}") if not ok: - fail += 1 - print(f" {backend} {name}: {detail}", file=sys.stderr) + failures.append(Failure(name, backend, reason, err)) + reasons.append(f" {backend}: {reason}") print(" ".join(row)) + for line in reasons: + print(line) print() - if fail: - print(f"{fail} failure(s)", file=sys.stderr) - return 1 - print("all parity tests pass") - return 0 + if not failures: + print("all parity tests pass") + return 0 + _report(failures) + if os.environ.get("GITHUB_ACTIONS") == "true": + _annotate(failures) + return 1 if __name__ == "__main__": diff --git a/pyre/extra_tests/parity_tests/structseq_type_identity.py b/pyre/extra_tests/parity_tests/structseq_type_identity.py new file mode 100644 index 00000000000..24fd89669a0 --- /dev/null +++ b/pyre/extra_tests/parity_tests/structseq_type_identity.py @@ -0,0 +1,65 @@ +"""A structseq type is built once, not rebuilt for each answer. + +`PyStructSequence_InitType2` runs at module init and the type it leaves is what +every later result is an instance of, so `type(os.stat(a)) is type(os.stat(b))` +and an `isinstance` check written against one answer holds for the next. A +constructor that builds its type inside the call instead returns a fresh class +each time: the values compare equal, the types never do, and the failure only +shows up in code that kept a type from an earlier call. + +Each name is asked for twice and skipped when the platform does not carry it — +`statvfs`/`uname` are POSIX, `getwindowsversion` is Windows, and a terminal size +is not a question a redirected stdout can answer. +""" + +import os +import sys +import time + + +def check(cond, what): + if not cond: + raise AssertionError(what) + + +def answers(): + """Every structseq this platform can be asked for, as a name and a thunk.""" + yield "os.stat_result", lambda: os.stat(sys.executable) + yield "os.times_result", os.times + yield "time.struct_time", time.localtime + yield "sys.version_info", lambda: sys.version_info + yield "sys.flags", lambda: sys.flags + yield "os.terminal_size", os.get_terminal_size + if hasattr(os, "statvfs"): + yield "os.statvfs_result", lambda: os.statvfs(".") + if hasattr(os, "uname"): + yield "os.uname_result", os.uname + if hasattr(sys, "getwindowsversion"): + yield "sys.getwindowsversion", sys.getwindowsversion + + +asked = 0 +for name, call in answers(): + try: + first = call() + except (AttributeError, OSError): + # Not a question this host can answer; nothing to compare. + continue + second = call() + asked += 1 + check( + type(first) is type(second), + f"{name} built a second type for its second answer", + ) + # The type is a tuple subclass, which is what makes the sequence half of a + # structseq work at all — and what a fresh-per-call type would still get + # right, so it is checked beside the identity rather than instead of it. + check(isinstance(first, tuple), f"{name} is not a tuple subclass") + check( + type(first).__name__ == type(second).__name__, + f"{name} disagreed with itself about its own name", + ) + +check(asked >= 5, f"only {asked} structseq(s) were reachable to compare") + +print("OK") diff --git a/pyre/extra_tests/parity_tests/surrogate_name_messages.py b/pyre/extra_tests/parity_tests/surrogate_name_messages.py index adf24082ae1..99246fb5f7c 100644 --- a/pyre/extra_tests/parity_tests/surrogate_name_messages.py +++ b/pyre/extra_tests/parity_tests/surrogate_name_messages.py @@ -19,6 +19,19 @@ FFFD = "�" +def at(obj): + """The address a repr quotes, in the spelling this platform's repr uses. + + `PyUnicode_FromFormat`'s `%p` hands the pointer to the platform's own + `printf` and normalizes only the prefix, so the MSVC runtime's padding and + upper case are part of the repr there and glibc's bare lower case is part + of it everywhere else. + """ + if sys.platform == "win32": + return "0x%0*X" % (2 * (8 if sys.maxsize > 2**32 else 4), id(obj)) + return "0x%x" % id(obj) + + class Repr: def __repr__(self): return S @@ -29,7 +42,7 @@ def f(a): return a f.__qualname__ = S - assert repr(f) == "" % (S, id(f)), ascii(repr(f)) + assert repr(f) == "" % (S, at(f)), ascii(repr(f)) try: f(1, 2) @@ -86,13 +99,13 @@ def g(): def check_contextvars(): var = contextvars.ContextVar("n", default=Repr()) var_repr = repr(var) - assert var_repr == "" % (S, id(var)), ascii(var_repr) + assert var_repr == "" % (S, at(var)), ascii(var_repr) token = var.set("x") - assert repr(token) == "" % (var_repr, id(token)), ascii(repr(token)) + assert repr(token) == "" % (var_repr, at(token)), ascii(repr(token)) var.reset(token) used_repr = repr(token) - assert used_repr == "" % (var_repr, id(token)), ascii(used_repr) + assert used_repr == "" % (var_repr, at(token)), ascii(used_repr) try: var.reset(token) except RuntimeError as e: diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index 11b08c68462..31bdcc72797 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -1723,7 +1723,10 @@ fn memoryview_repr(args: &[PyObjectRef]) -> Result } else { "memory" }; - Ok(w_str_new(&format!("<{label} at {mv:?}>"))) + Ok(w_str_new(&format!( + "<{label} at {}>", + crate::display::repr_addr(mv as usize) + ))) } /// Drop an mmap-backed view's export directly, bypassing any Python-callable @@ -16279,7 +16282,7 @@ pub fn is_builtin_divmod_function(callable: PyObjectRef) -> bool { } /// `divmod(a, b)` — pypy/interpreter/baseobjspace.py divmod row. -fn builtin_divmod(args: &[PyObjectRef]) -> Result { +pub(crate) fn builtin_divmod(args: &[PyObjectRef]) -> Result { let (args, kwargs) = split_builtin_kwargs(args); if has_real_kwargs(kwargs) { return Err(crate::PyError::type_error( diff --git a/pyre/pyre-interpreter/src/display.rs b/pyre/pyre-interpreter/src/display.rs index 434eb14f56a..4ae17007ada 100644 --- a/pyre/pyre-interpreter/src/display.rs +++ b/pyre/pyre-interpreter/src/display.rs @@ -11,6 +11,24 @@ use crate::{ builtin_code_name, function_get_name, function_get_qualname, }; +/// The address a repr quotes, spelled the way `PyUnicode_FromFormat`'s `%p` +/// spells it. +/// +/// That conversion hands the pointer to the platform's own `printf` and +/// normalizes only the prefix — "guaranteed to start with the literal `0x` +/// regardless of what the platform's `printf` yields". The platforms disagree +/// about the rest: the MSVC runtime pads to the pointer width and uppercases, +/// glibc does neither. So `` and +/// `` are the same repr, each on its own platform, +/// and Rust's `{:p}` is only ever the second one. +pub(crate) fn repr_addr(addr: usize) -> String { + if cfg!(windows) { + format!("0x{addr:0width$X}", width = size_of::() * 2) + } else { + format!("0x{addr:x}") + } +} + /// Try to call a dunder method (__repr__, __str__, etc.) on an instance, /// returning the raw result object when it is a `str`. pub(crate) unsafe fn try_call_dunder_obj( @@ -755,7 +773,7 @@ pub unsafe fn py_repr_wtf8(obj: PyObjectRef) -> Result // which substitutes U+FFFD for a lone surrogate. let mut repr = Wtf8Buf::from_string("")); + repr.push_str(&format!(" at {}>", crate::display::repr_addr(obj as usize))); return Ok(repr); } else if unsafe { pyre_object::is_exception(obj) } { // A user subclass that overrides `__repr__` shadows the builtin @@ -956,7 +974,7 @@ pub unsafe fn py_repr_wtf8(obj: PyObjectRef) -> Result } else { "memory" }; - format!("<{label} at {obj:?}>") + format!("<{label} at {}>", repr_addr(obj as usize)) } else if std::ptr::eq(tp, &INSTANCE_TYPE as *const PyType) { // Try __repr__ first, then __str__ if let Some(w) = try_call_dunder_wtf8(obj, "__repr__")? { @@ -966,7 +984,7 @@ pub unsafe fn py_repr_wtf8(obj: PyObjectRef) -> Result return Ok(w); } let name = crate::baseobjspace::getfulltypename(obj); - format!("<{name} object at {obj:?}>") + format!("<{name} object at {}>", repr_addr(obj as usize)) } else { // A builtin type carrying its own `__repr__` dict entry (e.g. // `_struct.Struct`) — dispatch it before the generic @@ -987,7 +1005,7 @@ pub unsafe fn py_repr_wtf8(obj: PyObjectRef) -> Result } } let name = crate::baseobjspace::getfulltypename(obj); - format!("<{name} object at {obj:?}>") + format!("<{name} object at {}>", repr_addr(obj as usize)) }; Ok(Wtf8Buf::from_string(formatted)) } diff --git a/pyre/pyre-interpreter/src/function.rs b/pyre/pyre-interpreter/src/function.rs index 98a5c5ea358..5c09f76f3c5 100644 --- a/pyre/pyre-interpreter/src/function.rs +++ b/pyre/pyre-interpreter/src/function.rs @@ -989,7 +989,10 @@ pub unsafe fn builtin_function_repr_text(name: &str, w_self: PyObjectRef) -> Str let type_name = crate::typedef::r#type(w_self) .map(|tp| unsafe { pyre_object::w_type_get_name(tp.as_ptr()) }) .unwrap_or("object"); - format!("") + format!( + "", + crate::display::repr_addr(w_self as usize) + ) } /// CPython 3.14 `meth_reduce`: type-bound builtins reconstruct through diff --git a/pyre/pyre-interpreter/src/importing.rs b/pyre/pyre-interpreter/src/importing.rs index 42f208dba74..abb7d80a2e6 100644 --- a/pyre/pyre-interpreter/src/importing.rs +++ b/pyre/pyre-interpreter/src/importing.rs @@ -827,14 +827,33 @@ fn init_string_module(ns: PyObjectRef) { ); } -/// `_sysconfig` stub — exposes `config_vars()` returning an empty dict. On -/// POSIX `sysconfig` only consults this for the build variables that pyre does -/// not generate; importing it is enough to satisfy `test_sysconfig`. +/// `_sysconfig` — `config_vars()`, the build variables that come from the +/// running binary rather than from a generated `_sysconfigdata_*` module. +/// +/// `sysconfig._init_non_posix` SUBSCRIPTS `Py_GIL_DISABLED` and `Py_DEBUG` to +/// spell `ABIFLAGS`, so on Windows a missing key is a `KeyError` out of the +/// first `get_config_var` call rather than the `None` the `.get()` readers +/// take. Both are 0 here — pyre is neither build, which is what an empty +/// `sys.abiflags` already says. The other two keys `config_vars` carries, +/// `EXT_SUFFIX` and `SOABI`, name an extension ABI; `_imp.extension_suffixes()` +/// is empty, so there is none to name and they stay absent for `.get()`. fn init_sysconfig_stub(ns: PyObjectRef) { crate::module_ns_store( ns, "config_vars", - crate::make_builtin_function("config_vars", |_| Ok(pyre_object::w_dict_new())), + crate::make_builtin_function("config_vars", |_| { + let vars = pyre_object::w_dict_new(); + unsafe { + for name in ["Py_DEBUG", "Py_GIL_DISABLED"] { + pyre_object::w_dict_store( + vars, + pyre_object::w_str_new(name), + pyre_object::w_int_new(0), + ); + } + } + Ok(vars) + }), ); } diff --git a/pyre/pyre-interpreter/src/module/_contextvars/mod.rs b/pyre/pyre-interpreter/src/module/_contextvars/mod.rs index 1b148654e70..ae1ea26f182 100644 --- a/pyre/pyre-interpreter/src/module/_contextvars/mod.rs +++ b/pyre/pyre-interpreter/src/module/_contextvars/mod.rs @@ -244,7 +244,7 @@ fn context_var_repr_string(obj: PyObjectRef) -> Result", obj as usize), + format!(" at {}>", crate::display::repr_addr(obj as usize)), )) } @@ -379,7 +379,7 @@ fn token_repr_string(token: PyObjectRef) -> Result", token as usize), + format!(" at {}>", crate::display::repr_addr(token as usize)), )) } diff --git a/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs b/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs index ec487c44fb6..035c46639f0 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs @@ -311,7 +311,8 @@ fn simplecdata_repr(args: &[PyObjectRef]) -> Result if !direct { let name = unsafe { pyre_object::typeobject::w_type_get_name(cls) }; return Ok(pyre_object::w_str_new(&format!( - "<{name} object at {obj:?}>" + "<{name} object at {}>", + crate::display::repr_addr(obj as usize) ))); } let tc = type_code_of(cls).ok_or_else(|| crate::PyError::type_error("abstract class"))?; diff --git a/pyre/pyre-interpreter/src/module/_locale/interp_locale.rs b/pyre/pyre-interpreter/src/module/_locale/interp_locale.rs index 65b91909f88..07b7c88440f 100644 --- a/pyre/pyre-interpreter/src/module/_locale/interp_locale.rs +++ b/pyre/pyre-interpreter/src/module/_locale/interp_locale.rs @@ -121,7 +121,10 @@ fn c_locale_conv() -> LocaleConvData { pub fn register_module(ns: pyre_object::PyObjectRef) { // Locale category constants sourced from libc so the values match // the host (Linux: LC_CTYPE=0; macOS: LC_ALL=0, LC_CTYPE=2; ...). - #[cfg(unix)] + // Windows has a C runtime too, and its numbering is a third one again + // (LC_ALL=0, LC_COLLATE=1, LC_CTYPE=2); a POSIX value passed to it names + // a different category, so it must come from libc there as well. + #[cfg(any(unix, windows))] { crate::module_ns_store( ns, @@ -144,14 +147,19 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "LC_MONETARY", pyre_object::w_int_new(libc::LC_MONETARY as i64), ); - crate::module_ns_store( - ns, - "LC_MESSAGES", - pyre_object::w_int_new(libc::LC_MESSAGES as i64), - ); crate::module_ns_store(ns, "LC_ALL", pyre_object::w_int_new(libc::LC_ALL as i64)); } - #[cfg(not(unix))] + // `LC_MESSAGES` is a POSIX category the MSVC CRT has no counterpart for. + // `locale.py:1984-1989` appends it to `__all__` only if the name survived + // its `from _locale import *`, so publishing it here on Windows puts a + // category into `from locale import *` that no call can be made with. + #[cfg(unix)] + crate::module_ns_store( + ns, + "LC_MESSAGES", + pyre_object::w_int_new(libc::LC_MESSAGES as i64), + ); + #[cfg(not(any(unix, windows)))] { crate::module_ns_store(ns, "LC_CTYPE", pyre_object::w_int_new(0)); crate::module_ns_store(ns, "LC_NUMERIC", pyre_object::w_int_new(1)); @@ -286,7 +294,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { } else { None }; - #[cfg(all(unix, feature = "host_env"))] + #[cfg(all(any(unix, windows), feature = "host_env"))] { let cat = (unsafe { pyre_object::w_int_get_value(args[0]) }) as i32; let c_locale = match locale_str.as_ref() { @@ -302,7 +310,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { None => Err(locale_error("unsupported locale setting")), } } - #[cfg(not(all(unix, feature = "host_env")))] + #[cfg(not(all(any(unix, windows), feature = "host_env")))] { // No libc available — every valid call resolves to the // POSIX "C" locale. `locale_str` is dropped on purpose. diff --git a/pyre/pyre-interpreter/src/module/_pickle/pickler.rs b/pyre/pyre-interpreter/src/module/_pickle/pickler.rs index a21f1ef2c92..1e82067dc57 100644 --- a/pyre/pyre-interpreter/src/module/_pickle/pickler.rs +++ b/pyre/pyre-interpreter/src/module/_pickle/pickler.rs @@ -1140,9 +1140,9 @@ fn fast_save_enter(ctx: &mut PickleCtx, obj_slot: usize) -> Result if is_cycle { ctx.fast_nesting -= 1; return Err(PyError::value_error(format!( - "fast mode: can't pickle cyclic objects including object type {} at {:p}", + "fast mode: can't pickle cyclic objects including object type {} at {}", crate::baseobjspace::object_functionstr_type_name(w_cur), - w_cur as *const u8, + crate::display::repr_addr(w_cur as usize), ))); } ctx.fast_memo.entry(h).or_default().push(obj_slot); diff --git a/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs b/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs index 72851c5dfa4..f086949852a 100644 --- a/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs +++ b/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs @@ -721,8 +721,10 @@ pub fn descr__repr__(args: &[PyObjectRef]) -> Result { }; let addr = w_self as usize; Ok(pyre_object::w_str_new(&format!( - "<{} at 0x{:x}{}>", - type_name, addr, state + "<{} at {}{}>", + type_name, + crate::display::repr_addr(addr), + state ))) } diff --git a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs index e56f56212cc..162d75e079a 100644 --- a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs +++ b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs @@ -1650,6 +1650,39 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ) } + /// The width the platform's truncating call takes its length in. + #[cfg(windows)] + type TruncateLen = i64; + #[cfg(not(windows))] + type TruncateLen = libc::off_t; + + /// `space.int_w` over the `r_longlong` half of `interp_posix.py:404`. + /// + /// `Py_off_t_converter` names the C type it could not fit the value + /// into, and that type is the platform's: a `long` where the converter + /// is `PyLong_AsLong`, and nothing at all where it is + /// `PyLong_AsLongLong`. + fn truncate_length_w(obj: PyObjectRef) -> Result { + const TOO_BIG: &str = if cfg!(windows) { + "int too big to convert" + } else { + "Python int too large to convert to C long" + }; + let w_length = crate::baseobjspace::space_index(obj)?; + let length = crate::baseobjspace::int_w(w_length).map_err(|err| { + if err.kind == crate::PyErrorKind::OverflowError { + crate::PyError::overflow_error(TOO_BIG) + } else { + err + } + })?; + // `off_t` is the width the call takes the length in, and a value + // above it is not a size the file can be given. An `as` cast would + // wrap it into one the caller never asked for and truncate the file + // to that instead. + TruncateLen::try_from(length).map_err(|_| crate::PyError::overflow_error(TOO_BIG)) + } + fn fs_err_with_filename(e: std::io::Error, w_path: PyObjectRef) -> crate::PyError { fs_err_with_filename2(e, 0, w_path, pyre_object::PY_NULL) } @@ -2493,8 +2526,16 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { if !unsafe { pyre_object::is_tuple(obj) } || unsafe { pyre_object::w_tuple_len(obj) } != 2 { + // `times` is the argument that also has a `None` spelling + // — it is the one whose default means "now" — and its + // message names that spelling. `ns` has no such form. + let shape = if what == "times" { + "either a tuple of two ints or None" + } else { + "a tuple of two ints" + }; return Err(crate::PyError::type_error(format!( - "utime: '{what}' must be a tuple of two ints" + "utime: '{what}' must be {shape}" ))); } Ok(( @@ -2502,6 +2543,15 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { unsafe { pyre_object::w_tuple_getitem(obj, 1) }.unwrap(), )) }; + /// The out-of-range answer for both spellings of a second. + /// + /// `_PyTime_ObjectToDenominator` refuses a value no `time_t` can hold + /// by overflow rather than by value, and `_PyLong_AsTime_t` gives the + /// same words for an integer too wide to be one — `(2**200, 0)` and + /// `(1e30, 0)` answer alike. + fn time_t_overflow() -> crate::PyError { + crate::PyError::overflow_error("timestamp out of range for platform time_t") + } // `_PyTime_ObjectToTimespec(..., _PyTime_ROUND_FLOOR)`: the seconds are // the floor of the value and the nanoseconds are what is left above // that floor, so they stay in `0..1_000_000_000` however negative the @@ -2509,34 +2559,88 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { // `(-3, 500000000)`, which reads back as `-1_500_000_000` and // `-2_500_000_000` nanoseconds. let time_from_secs = |v: PyObjectRef| -> Result { - let f = crate::builtins::builtin_float(&[v])?; + // An integer names its second exactly, so it is read as one + // rather than through a float that would round the seconds it is + // too wide to hold. + if unsafe { pyre_object::is_int_or_long(v) } { + let sec = crate::builtins::space_index_w(v).map_err(|_| time_t_overflow())?; + return Ok(UTime { sec, nsec: 0 }); + } + // Everything else has to name a float. What cannot is not a time + // at all, and is refused by type: `utime(p, ('a', 'b'))` names the + // type it was given rather than reporting a failed float parse. + let f = crate::builtins::builtin_float(&[v]).map_err(|err| { + if err.kind == crate::PyErrorKind::OverflowError { + time_t_overflow() + } else { + crate::PyError::type_error(format!( + "argument must be int or float, not {}", + crate::type_methods::arg_type_name(v) + )) + } + })?; let secs = unsafe { pyre_object::w_float_get_value(f) }; + // A NaN has no floor to take, and it is the one non-finite value + // answered by value rather than by range. + if secs.is_nan() { + return Err(crate::PyError::value_error( + "Invalid value NaN (not a number)", + )); + } let floor = secs.floor(); - // The floor of a non-finite or too-large value is not a second any + // The floor of an infinite or too-large value is not a second any // clock names; `i64::MIN`/`MAX` are what an `as` cast would answer // for both, so the range is checked before the cast rather than // read back out of it. if !(floor >= -(2f64.powi(63)) && floor < 2f64.powi(63)) { - return Err(crate::PyError::value_error("utime: timestamp out of range")); + return Err(time_t_overflow()); } let mut sec = floor as i64; let mut nsec = ((secs - floor) * 1e9).floor() as i64; if nsec >= 1_000_000_000 { nsec -= 1_000_000_000; - sec = sec - .checked_add(1) - .ok_or_else(|| crate::PyError::value_error("utime: timestamp out of range"))?; + sec = sec.checked_add(1).ok_or_else(time_t_overflow)?; } Ok(UTime { sec, nsec }) }; let time_from_ns = |v: PyObjectRef| -> Result { - let n = crate::builtins::space_index_w(v)?; - // `split_py_long_to_s_and_ns` divides by `1_000_000_000` the way - // Python's own `//` and `%` do, so a negative count of nanoseconds + // `split_py_long_to_s_and_ns` splits with `divmod` before it + // narrows anything, so a count of nanoseconds too wide for a + // `time_t` is only refused when the SECOND it names is — `ns=2**80` + // is a second that fits. Dividing after the narrowing turned away + // the whole range instead. `divmod` is also what answers for a + // value that is not a number at all. + let split = crate::builtins::builtin_divmod(&[ + v, + pyre_object::w_int_new(1_000_000_000), + ])?; + let (w_sec, w_nsec) = unsafe { + ( + pyre_object::w_tuple_getitem(split, 0), + pyre_object::w_tuple_getitem(split, 1), + ) + }; + let (Some(w_sec), Some(w_nsec)) = (w_sec, w_nsec) else { + return Err(crate::PyError::type_error( + "utime: divmod() returned a non-pair", + )); + }; + // Python's own `//` and `%`, so a negative count of nanoseconds // lands on the second below it with a positive remainder. + // + // Only an integer second can be out of a `time_t`'s range. A + // quotient that is not one at all — `divmod` answers a float pair + // for `ns=(1.5, 2.5)` — keeps the conversion's own refusal. + let sec = crate::builtins::space_index_w(w_sec).map_err(|err| { + if err.kind == crate::PyErrorKind::OverflowError { + time_t_overflow() + } else { + err + } + })?; Ok(UTime { - 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)?, }) }; @@ -2597,29 +2701,32 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "utime: dir_fd and follow_symlinks=False are unavailable on this platform", )); } - // A FILETIME counts 100-ns intervals from 1601-01-01, so a - // pre-epoch time is an ordinary positive count rather than one with - // no representation; utimensat and every POSIX host write it, and - // 3.14 writes it through `SetFileTime` (rposix.win32_utime). The - // sub-100ns of a nanosecond is not a tick the filesystem holds and - // is floored, as the call floors it. + // `time_t_to_FILE_TIME` (`rwin32file.py:320-323`): a FILETIME + // counts 100ns ticks from 1601-01-01, so shifting the epoch is what + // makes a second before 1970 an ordinary positive tick count rather + // than one with no representation. The sub-100ns of a nanosecond is + // not a tick the filesystem holds and is floored, as the conversion + // floors it — which is why `ns=-1` reads back as `-100`. + // + // The arithmetic wraps rather than checks: `r_longlong` there, and + // the same `__int64` in the C the line is a transcription of. A + // second this filesystem cannot hold writes the bits the + // multiplication leaves rather than being diagnosed, and a value no + // `time_t` can hold has already been refused by `time_from_secs`. const EPOCH_DIFF: i64 = 11_644_473_600; - let to_filetime = |t: UTime| -> Result { + let to_filetime = |t: UTime| -> FILETIME { let ticks = t .sec - .checked_add(EPOCH_DIFF) - .filter(|s| *s >= 0) - .and_then(|s| s.checked_mul(10_000_000)) - .and_then(|s| s.checked_add(t.nsec / 100)) - .ok_or_else(|| crate::PyError::value_error("utime: timestamp out of range"))? - as u64; - Ok(FILETIME { + .wrapping_add(EPOCH_DIFF) + .wrapping_mul(10_000_000) + .wrapping_add(t.nsec / 100) as u64; + FILETIME { dwLowDateTime: ticks as u32, dwHighDateTime: (ticks >> 32) as u32, - }) + } }; - let atime = to_filetime(access)?; - let mtime = to_filetime(modified)?; + let atime = to_filetime(access); + let mtime = to_filetime(modified); let wide = wide_path(&path.as_bytes)?; // FILE_WRITE_ATTRIBUTES is the access `SetFileTime` takes; // FILE_FLAG_BACKUP_SEMANTICS lets the name open a directory too. @@ -3111,9 +3218,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { meta.atime(), meta.mtime(), meta.ctime(), - meta.atime() * 1_000_000_000 + meta.atime_nsec(), - meta.mtime() * 1_000_000_000 + meta.mtime_nsec(), - meta.ctime() * 1_000_000_000 + meta.ctime_nsec(), + whole_ns(meta.atime(), meta.atime_nsec()), + whole_ns(meta.mtime(), meta.mtime_nsec()), + whole_ns(meta.ctime(), meta.ctime_nsec()), ) }; #[cfg(windows)] @@ -3167,12 +3274,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { let atime_secs = (meta.last_access_time() as i64 / 10_000_000) - EPOCH_DIFF; let mtime_secs = (meta.last_write_time() as i64 / 10_000_000) - EPOCH_DIFF; let ctime_secs = (meta.creation_time() as i64 / 10_000_000) - EPOCH_DIFF; - let atime_ns = - ((meta.last_access_time() as i64 % 10_000_000) * 100) + atime_secs * 1_000_000_000; - let mtime_ns = - ((meta.last_write_time() as i64 % 10_000_000) * 100) + mtime_secs * 1_000_000_000; - let ctime_ns = - ((meta.creation_time() as i64 % 10_000_000) * 100) + ctime_secs * 1_000_000_000; + let atime_ns = whole_ns(atime_secs, (meta.last_access_time() as i64 % 10_000_000) * 100); + let mtime_ns = whole_ns(mtime_secs, (meta.last_write_time() as i64 % 10_000_000) * 100); + let ctime_ns = whole_ns(ctime_secs, (meta.creation_time() as i64 % 10_000_000) * 100); ( mode, 0i64, // st_ino — not available on Windows 0i64, // st_dev @@ -3222,6 +3326,25 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { /// The `stat_result` fields, read out of whichever source produced them: /// `std::fs::Metadata` for the path and descriptor forms, `libc::stat` /// for the `fstatat` form a `dir_fd`-relative name takes. + /// A whole timestamp in nanoseconds. + /// + /// `i64` nanoseconds run out in 2262, and a file can carry a later time + /// than that — every Windows FILETIME up to the year 30828 is one. The + /// product is taken wide so `st_mtime_ns` is the number it is rather than + /// the wrap `sec * 1_000_000_000` would answer with, and + /// [`w_time_ns`] hands back an int of whatever width it needs. + fn whole_ns(sec: i64, nsec: i64) -> i128 { + sec as i128 * 1_000_000_000 + nsec as i128 + } + + /// The `st_*_ns` field as a Python int, which has no width to run out of. + fn w_time_ns(ns: i128) -> pyre_object::PyObjectRef { + match i64::try_from(ns) { + Ok(n) => pyre_object::w_int_new(n), + Err(_) => pyre_object::longobject::w_long_new(pyre_object::rbigint::RBigInt::from(ns)), + } + } + struct StatFields { mode: i64, ino: i64, @@ -3233,9 +3356,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { atime: i64, mtime: i64, ctime: i64, - atime_ns: i64, - mtime_ns: i64, - ctime_ns: i64, + atime_ns: i128, + mtime_ns: i128, + ctime_ns: i128, #[cfg(unix)] blksize: i64, #[cfg(unix)] @@ -3294,31 +3417,31 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { // `_ll_get_st_atime` — float times keep sub-second precision: // `float(seconds) + 1e-9 * nanosecond_fraction`, where the // fraction is recovered from the full-nanosecond field. - let st_atime_f = st_atime as f64 + 1e-9 * (st_atime_ns - st_atime * 1_000_000_000) as f64; - let st_mtime_f = st_mtime as f64 + 1e-9 * (st_mtime_ns - st_mtime * 1_000_000_000) as f64; - let st_ctime_f = st_ctime as f64 + 1e-9 * (st_ctime_ns - st_ctime * 1_000_000_000) as f64; + let st_atime_f = st_atime as f64 + 1e-9 * (st_atime_ns - whole_ns(st_atime, 0)) as f64; + let st_mtime_f = st_mtime as f64 + 1e-9 * (st_mtime_ns - whole_ns(st_mtime, 0)) as f64; + let st_ctime_f = st_ctime as f64 + 1e-9 * (st_ctime_ns - whole_ns(st_ctime, 0)) as f64; #[allow(unused_mut)] let mut extras = vec![ ("st_atime", pyre_object::w_float_new(st_atime_f)), ("st_mtime", pyre_object::w_float_new(st_mtime_f)), ("st_ctime", pyre_object::w_float_new(st_ctime_f)), - ("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)), + ("st_mtime_ns", w_time_ns(st_mtime_ns)), + ("st_ctime_ns", w_time_ns(st_ctime_ns)), // `build_stat_result` (interp_posix.py:554-557): the // sub-second remainder of each full-nanosecond timestamp, // `value % 1_000_000_000` (non-negative for pre-1970 times). ( "nsec_atime", - pyre_object::w_int_new(st_atime_ns.rem_euclid(1_000_000_000)), + pyre_object::w_int_new(st_atime_ns.rem_euclid(1_000_000_000) as i64), ), ( "nsec_mtime", - pyre_object::w_int_new(st_mtime_ns.rem_euclid(1_000_000_000)), + pyre_object::w_int_new(st_mtime_ns.rem_euclid(1_000_000_000) as i64), ), ( "nsec_ctime", - pyre_object::w_int_new(st_ctime_ns.rem_euclid(1_000_000_000)), + pyre_object::w_int_new(st_ctime_ns.rem_euclid(1_000_000_000) as i64), ), ]; #[cfg(unix)] @@ -3343,9 +3466,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { let st_atime = st.atime; let st_mtime = st.mtime; let st_ctime = st.ctime; - let st_atime_ns = st.atime * 1_000_000_000 + st.atime_nsec; - let st_mtime_ns = st.mtime * 1_000_000_000 + st.mtime_nsec; - let st_ctime_ns = st.ctime * 1_000_000_000 + st.ctime_nsec; + let st_atime_ns = whole_ns(st.atime, st.atime_nsec); + let st_mtime_ns = whole_ns(st.mtime, st.mtime_nsec); + let st_ctime_ns = whole_ns(st.ctime, st.ctime_nsec); let seq = vec![ pyre_object::w_int_new(st.mode as i64), pyre_object::w_int_new(st.ino as i64), @@ -3358,28 +3481,28 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { pyre_object::w_int_new(st_mtime), pyre_object::w_int_new(st_ctime), ]; - let st_atime_f = st_atime as f64 + 1e-9 * (st_atime_ns - st_atime * 1_000_000_000) as f64; - let st_mtime_f = st_mtime as f64 + 1e-9 * (st_mtime_ns - st_mtime * 1_000_000_000) as f64; - let st_ctime_f = st_ctime as f64 + 1e-9 * (st_ctime_ns - st_ctime * 1_000_000_000) as f64; + let st_atime_f = st_atime as f64 + 1e-9 * (st_atime_ns - whole_ns(st_atime, 0)) as f64; + let st_mtime_f = st_mtime as f64 + 1e-9 * (st_mtime_ns - whole_ns(st_mtime, 0)) as f64; + let st_ctime_f = st_ctime as f64 + 1e-9 * (st_ctime_ns - whole_ns(st_ctime, 0)) as f64; #[allow(unused_mut)] let mut extras = vec![ ("st_atime", pyre_object::w_float_new(st_atime_f)), ("st_mtime", pyre_object::w_float_new(st_mtime_f)), ("st_ctime", pyre_object::w_float_new(st_ctime_f)), - ("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)), + ("st_mtime_ns", w_time_ns(st_mtime_ns)), + ("st_ctime_ns", w_time_ns(st_ctime_ns)), ( "nsec_atime", - pyre_object::w_int_new(st_atime_ns.rem_euclid(1_000_000_000)), + pyre_object::w_int_new(st_atime_ns.rem_euclid(1_000_000_000) as i64), ), ( "nsec_mtime", - pyre_object::w_int_new(st_mtime_ns.rem_euclid(1_000_000_000)), + pyre_object::w_int_new(st_mtime_ns.rem_euclid(1_000_000_000) as i64), ), ( "nsec_ctime", - pyre_object::w_int_new(st_ctime_ns.rem_euclid(1_000_000_000)), + pyre_object::w_int_new(st_ctime_ns.rem_euclid(1_000_000_000) as i64), ), ("st_blksize", pyre_object::w_int_new(st.blksize as i64)), ("st_blocks", pyre_object::w_int_new(st.blocks as i64)), @@ -3501,9 +3624,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { atime: st.st_atime as i64, mtime: st.st_mtime as i64, ctime: st.st_ctime as i64, - atime_ns: st.st_atime as i64 * 1_000_000_000 + st.st_atime_nsec as i64, - mtime_ns: st.st_mtime as i64 * 1_000_000_000 + st.st_mtime_nsec as i64, - ctime_ns: st.st_ctime as i64 * 1_000_000_000 + st.st_ctime_nsec as i64, + atime_ns: whole_ns(st.st_atime as i64, st.st_atime_nsec as i64), + mtime_ns: whole_ns(st.st_mtime as i64, st.st_mtime_nsec as i64), + ctime_ns: whole_ns(st.st_ctime as i64, st.st_ctime_nsec as i64), blksize: st.st_blksize as i64, blocks: st.st_blocks as i64, rdev: st.st_rdev as i64, @@ -3534,9 +3657,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { atime: st.st_atime as i64, mtime: st.st_mtime as i64, ctime: st.st_birthtime as i64, - atime_ns: st.st_atime as i64 * 1_000_000_000 + st.st_atime_nsec as i64, - mtime_ns: st.st_mtime as i64 * 1_000_000_000 + st.st_mtime_nsec as i64, - ctime_ns: st.st_birthtime as i64 * 1_000_000_000 + st.st_birthtime_nsec as i64, + atime_ns: whole_ns(st.st_atime as i64, st.st_atime_nsec as i64), + mtime_ns: whole_ns(st.st_mtime as i64, st.st_mtime_nsec as i64), + ctime_ns: whole_ns(st.st_birthtime as i64, st.st_birthtime_nsec as i64), } } @@ -5727,25 +5850,6 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { } } - /// `space.int_w` over the `r_longlong` half of `interp_posix.py:404`. - #[cfg(all(unix, not(feature = "sandbox")))] - fn truncate_length_w(obj: PyObjectRef) -> Result { - let w_length = crate::baseobjspace::space_index(obj)?; - let length = crate::baseobjspace::int_w(w_length).map_err(|err| { - if err.kind == crate::PyErrorKind::OverflowError { - crate::PyError::overflow_error("Python int too large to convert to C long") - } else { - err - } - })?; - // `off_t` is the width the call takes the length in, and a value - // above it is not a size the file can be given. An `as` cast would - // wrap it into one the caller never asked for and truncate the file - // to that instead. - libc::off_t::try_from(length).map_err(|_| { - crate::PyError::overflow_error("Python int too large to convert to C long") - }) - } // os.truncate(path, length) -> None // @@ -8014,7 +8118,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "ftruncate() requires 2 arguments", )); } - let length = crate::baseobjspace::int_w(args[1])?; + let length = truncate_length_w(args[1])?; crt_result(crt_fd::ftruncate(borrowed_fd(args[0])?, length)) }, 2, @@ -8035,7 +8139,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { return Err(crate::PyError::type_error("truncate() requires 2 arguments")); } let path = crate::gateway::fsencode_path_or_fd_w(args[0], "truncate", true)?; - let length = crate::baseobjspace::int_w(args[1])?; + let length = truncate_length_w(args[1])?; if path.as_fd != -1 { let bfd = unsafe { crt_fd::Borrowed::borrow_raw(path.as_fd) }; return crt_result(crt_fd::ftruncate(bfd, length)); diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index ea758b2c318..b5101ac3bd8 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -1124,6 +1124,13 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { // reads it to build USER_SITE. #[cfg(windows)] module_ns_store(ns, "winver", w_str_new("3.14")); + // sys._vpath — the build's relative path from the executable's directory to + // the prefix. `sysconfig._init_config_vars` subscripts it under `os.name == + // 'nt'`, so it is an AttributeError out of the first `get_config_var` call + // when absent; it is stored into `_CONFIG_VARS['VPATH']` and read nowhere + // else, `sys._stdlib_dir` being what locates the stdlib here. + #[cfg(windows)] + module_ns_store(ns, "_vpath", w_str_new(r"..\..")); module_ns_store( ns, "byteorder", @@ -1337,6 +1344,81 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "getdefaultencoding", make_builtin_function_with_arity("getdefaultencoding", |_| Ok(w_str_new("utf-8")), 0), ); + // sys.getwindowsversion — a five-field sequence over `OSVERSIONINFOEXW` + // with five named-only fields beyond it, the same shape `os.stat_result` + // carries its `st_*_ns` extras in. `test.support.os_helper` reads + // `.platform` at import, so every `test.support` consumer needs it. + // + // `major`/`minor`/`build` are kernel32's own file version rather than + // `GetVersionEx`'s answer: that call reports the version an unmanifested + // binary is shimmed to, and only an application manifest declaring + // compatibility makes it report the running one. `platform_version` — the + // field that exists because of exactly that shimming — therefore agrees + // with them here instead of correcting them. + #[cfg(windows)] + module_ns_store( + ns, + "getwindowsversion", + make_builtin_function_with_arity( + "getwindowsversion", + |args| { + if !args.is_empty() { + return Err(crate::PyError::type_error( + "getwindowsversion() takes no arguments", + )); + } + let info = rustpython_host_env::windows::get_windows_version().map_err(|e| { + crate::PyError::os_error_win32_syscall2( + e.raw_os_error().unwrap_or(0), + pyre_object::PY_NULL, + pyre_object::PY_NULL, + ) + })?; + // 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 = 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; + Ok(crate::_structseq::new_instance_with_extra( + cls, + vec![ + w_int_new(info.major as i64), + w_int_new(info.minor as i64), + w_int_new(info.build as i64), + w_int_new(info.platform as i64), + w_str_new(&info.service_pack), + ], + vec![ + ("service_pack_major", w_int_new(info.service_pack_major as i64)), + ("service_pack_minor", w_int_new(info.service_pack_minor as i64)), + ("suite_mask", w_int_new(info.suite_mask as i64)), + ("product_type", w_int_new(info.product_type as i64)), + ( + "platform_version", + 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), + ]), + ), + ], + )) + }, + 0, + ), + ); // sys.getrecursionlimit / setrecursionlimit — pypy/module/sys/vm.py:45. // The runtime stack budget lives in `crate::stack_check`; both // helpers route through it so the interpreter, JIT prologue probe, diff --git a/pyre/pyre-interpreter/src/module/thread/mod.rs b/pyre/pyre-interpreter/src/module/thread/mod.rs index d044229e872..b2e412d05d3 100644 --- a/pyre/pyre-interpreter/src/module/thread/mod.rs +++ b/pyre/pyre-interpreter/src/module/thread/mod.rs @@ -678,7 +678,10 @@ mod lock_class { fn __repr__(&self) -> String { let state = if self.locked() { "locked" } else { "unlocked" }; - format!("<{state} _thread.lock object at {:p}>", self) + format!( + "<{state} _thread.lock object at {}>", + crate::display::repr_addr(self as *const Self as usize) + ) } fn _at_fork_reinit(&self) { @@ -907,8 +910,10 @@ mod rlock_class { "locked" }; format!( - "<{locked} _thread.RLock object owner={} count={} at {:p}>", - state.owner, state.count, self + "<{locked} _thread.RLock object owner={} count={} at {}>", + state.owner, + state.count, + crate::display::repr_addr(self as *const Self as usize) ) } diff --git a/pyre/pyre-interpreter/src/pycode.rs b/pyre/pyre-interpreter/src/pycode.rs index 2e4fcf42489..2af8575fe28 100644 --- a/pyre/pyre-interpreter/src/pycode.rs +++ b/pyre/pyre-interpreter/src/pycode.rs @@ -1180,8 +1180,9 @@ pub unsafe fn code_repr(obj: PyObjectRef) -> Result let raw_line = (*(obj as *const PyCode)).co_firstlineno_raw as i64; let line = if raw_line == 0 { -1 } else { raw_line }; let mut repr = rustpython_wtf8::Wtf8Buf::from_string(format!( - " rustpython_wtf8::Wtf8Buf { let code = self.code(); let mut out = rustpython_wtf8::Wtf8Buf::from_string(format!( - "")); + repr.push_str(&format!( + " at {}>", + crate::display::repr_addr(function as usize) + )); Ok(pyre_object::w_str_from_wtf8(repr)) }, 1, @@ -13929,7 +13932,8 @@ fn init_method_wrapper_type(ns: PyObjectRef) { .map(|tp| unsafe { pyre_object::w_type_get_name(tp.as_ptr()) }) .unwrap_or("object"); Ok(pyre_object::w_str_new(&format!( - "" + "", + crate::display::repr_addr(w_self as usize) ))) }, 1, @@ -15060,15 +15064,19 @@ fn cell_descr_repr(args: &[PyObjectRef]) -> crate::PyResult { } let value = unsafe { pyre_object::w_cell_get(cell) }; let text = if value.is_null() { - format!("", cell as usize) + format!( + "", + crate::display::repr_addr(cell as usize) + ) } else { let type_name = crate::typedef::r#type(value) .map(|tp| unsafe { pyre_object::w_type_get_name(tp.as_ptr()) }) .unwrap_or_else(|| unsafe { (*(*value).ob_type).name }); let type_name: String = type_name.chars().take(80).collect(); format!( - "", - cell as usize, value as usize + "", + crate::display::repr_addr(cell as usize), + crate::display::repr_addr(value as usize) ) }; Ok(w_str_new(&text)) @@ -18778,14 +18786,15 @@ fn init_object_type(ns: PyObjectRef) { // `w_obj.getrepr(space, '%s object' % fulltypename)`. let name = crate::baseobjspace::getfulltypename(obj); return Ok(pyre_object::w_str_new_managed(&format!( - "<{name} object at {obj:?}>" + "<{name} object at {}>", + crate::display::repr_addr(obj as usize) ))); } } // For non-instances, delegate to display Ok(pyre_object::w_str_new_managed(&format!( - "", - obj + "", + crate::display::repr_addr(obj as usize) ))) }, 1, @@ -25232,27 +25241,27 @@ fn generator_frame(obj: PyObjectRef) -> *mut crate::pyframe::PyFrame { fn generator_descr_repr(args: &[PyObjectRef]) -> crate::PyResult { let name = generator_name_value(args[0], true)?; Ok(w_str_new(&format!( - "", + "", unsafe { pyre_object::w_str_get_value(name) }, - args[0] + crate::display::repr_addr(args[0] as usize) ))) } fn coroutine_descr_repr(args: &[PyObjectRef]) -> crate::PyResult { let name = generator_name_value(args[0], true)?; Ok(w_str_new(&format!( - "", + "", unsafe { pyre_object::w_str_get_value(name) }, - args[0] + crate::display::repr_addr(args[0] as usize) ))) } fn async_generator_descr_repr(args: &[PyObjectRef]) -> crate::PyResult { let name = generator_name_value(args[0], true)?; Ok(w_str_new(&format!( - "", + "", unsafe { pyre_object::w_str_get_value(name) }, - args[0] + crate::display::repr_addr(args[0] as usize) ))) } diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs index 91c7a8537e0..8c6a39abb1a 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs @@ -8265,6 +8265,22 @@ fn walker_guard_exact_w_class( if expected_typeobj.is_null() || ctx.trace_ctx.heap_cache().is_unescaped(obj) { return Ok(()); } + // Every predicate that admits one of these folds — `is_exact_builtin_instance`, + // `is_plain_int1`, [`walker_exact_builtin_class`] — treats a null `w_class` as a + // second spelling of "exact builtin", while the guard below reads the slot and + // pins a single value. Pinning the canonical against an operand that carries + // the null spelling emits a guard the recorded operand itself fails, and nothing + // writes the slot afterwards, so it fails on every execution without ever + // converging — one bridge per `trace_eagerness` bucket. Establishing that the + // operand carries what is pinned is the caller's, which is what this checks. + debug_assert!( + walker_concrete_ref_object(ctx, obj).is_none_or(|concrete| { + (pyre_object::tagged_int::CAN_BE_TAGGED + && pyre_object::tagged_int::is_tagged_int(concrete)) + || std::ptr::eq(unsafe { (*concrete).w_class }, expected_typeobj) + }), + "guard_exact_w_class at pc={op_pc} would pin a `w_class` its recorded operand does not carry", + ); let actual = crate::state::opimpl_getfield_gc_r(ctx.trace_ctx, obj, crate::descr::w_class_descr()); let expected = ctx.trace_ctx.const_ref(expected_typeobj as i64); diff --git a/pyre/pyrex/src/lib.rs b/pyre/pyrex/src/lib.rs index 14f395d2b97..9167175ee6e 100644 --- a/pyre/pyrex/src/lib.rs +++ b/pyre/pyrex/src/lib.rs @@ -1246,17 +1246,26 @@ fn finalize_system_exit( /// Die by SIGINT for an uncaught KeyboardInterrupt after shutdown has run /// (`app_main.py:1133-1153`). fn terminate_by_sigint() -> ! { - unsafe { - libc::signal(libc::SIGINT, libc::SIG_DFL); - #[cfg(windows)] - let signaled = libc::raise(libc::SIGINT); - #[cfg(not(windows))] - let signaled = libc::kill(libc::getpid(), libc::SIGINT); - if signaled != 0 { - std::process::exit(1); + // A Win32 process has no SIGINT to die of: restoring `SIG_DFL` and calling + // `raise(SIGINT)` runs the CRT's default action, which returns and ends the + // process with status 3 rather than the interrupt a shell reads. The status + // that reads as one is `STATUS_CONTROL_C_EXIT`, so it is exited with + // directly. Both callers have already finalized and flushed. + #[cfg(windows)] + { + const STATUS_CONTROL_C_EXIT: u32 = 0xC000_013A; + std::process::exit(STATUS_CONTROL_C_EXIT as i32); + } + #[cfg(not(windows))] + { + unsafe { + libc::signal(libc::SIGINT, libc::SIG_DFL); + if libc::kill(libc::getpid(), libc::SIGINT) != 0 { + std::process::exit(1); + } } + std::process::abort(); } - std::process::abort(); } fn is_keyboard_interrupt(error: &pyre_interpreter::PyError) -> bool {