Skip to content

posix: argument binding across the module surface, plus a lone-surrogate abort - #1113

Merged
youknowone merged 17 commits into
mainfrom
rewrite-tracer
Aug 10, 2026
Merged

posix: argument binding across the module surface, plus a lone-surrogate abort#1113
youknowone merged 17 commits into
mainfrom
rewrite-tracer

Conversation

@youknowone

@youknowone youknowone commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Ten commits of posix parity work. The through-line for most of them is
argument binding: entry points registered without a Signature never had
the trailing __pyre_kw__ marker dict split off the argument slice, so a
keyword either vanished or arrived as a positional value, and a surplus
positional was dropped rather than refused.

What was actually broken

  • os.dup2(fd, fd2, inheritable=False) returned an inheritable descriptor.
    The marker dict landed in the third positional slot and read truthy. Nothing
    raised and the return value looked right; only os.get_inheritable showed it.
  • os.symlink(src, dst, target_is_directory, dir_fd) created the link and
    returned None where CPython raises. While probing this, it left a stray
    b -> a symlink in the repository root.
  • os.access(path, mode, zzz=1) succeeded; os.listdir(d, 1) returned the
    listing; os.listdir(path=d) and os.readlink(path=d) raised, the latter
    reaching c_int_w with the kwargs dict itself.
  • func.__name__ = <str with a lone surrogate> aborted the process at
    unicodeobject.rs:579. Reachable from ordinary code: every OS-supplied name
    is decoded with surrogateescape, and sys.argv carries one on macOS.

Of twenty measured posix calls, fifteen disagreed with CPython 3.14. All twenty
agree now, message text included.

Approach

interp_posix.rs already had bind_path_args from an earlier slice, and it
emits the argument-clinic spellings, so most of these get text parity today.
Added bind_posonly_args for the two shapes it did not cover — a parameter
list wholly before the / (parsed by _PyArg_CheckPositional, which refuses
every keyword and words its count differently), and a positional-only prefix
with a keyword-only tail (_PyArg_UnpackKeywords, parenthesised count).

dup2 went through #[pyre_function] instead, since it is registered outside
that helper's reach. Its guards live in the function body, which matters: the
residual JIT path enters builtins without the keyword binder.

Scope deliberately not taken

  • os.access now binds dir_fd / effective_ids / follow_symlinks but
    still answers none of them — the body calls access(2), not
    faccessat(2). Making them raise would be consistent with what
    os.supports_* advertises but would diverge from CPython on macOS, so
    today's semantics are kept and the capability gap is filed separately.
  • sendfile binds headers / trailers / flags; neither arm passes them on.
  • dup2's arity message is still the positional-only spelling. It is
    keyword-bindable upstream, so it belongs to the clinic family, but
    #[pyre_function] cannot express that until a #[posonly] marker exists.

Verification

Three new parity tests, all green on dynasm and cranelift, with a red control
recorded against the pre-fix binary for each. os_arg_binding_surface.py
asserts message text rather than just the exception type, because the two count
spellings are not interchangeable; it also checks symlink refused without
creating the link, with a positive control that symlink(dst=) still creates
one. str_lone_surrogate_names.py builds its surrogate from bytes and asserts
round-trip equality — the first fix turned the abort into silent data loss
(types.FunctionType(...) returned 'bad��name'), which a
"did not crash" assertion would have passed.

check.py --backend dynasm,cranelift,wasm: dynasm 404/404, cranelift 404/404,
wasm 400/400. The one red, synth/pypy_type_surface, is a pre-existing base
red on all three backends; its figures are byte-identical before and after
these commits (bridges_compiled 5 -> 102, guard_failures 1011 -> 20497) and
the bench imports only sys, so it cannot reach any of this. Not re-recorded.

The lone-surrogate work was measured from the producers rather than by auditing
the 165 w_str_get_value call sites: 108 operations across sys.argv (macOS)
and a real non-UTF-8 filename (Linux container — macOS refuses to create one,
EILSEQ). That found exactly one abort.

opened by Claude

Summary by CodeRabbit

  • New Features

    • Added POSIX scheduling, CPU affinity, processor-count, and pipe2 support.
    • Expanded filesystem, process, descriptor, environment, and platform compatibility.
    • Preserved lone-surrogate characters in function names and metadata.
    • Improved path argument errors, timestamp behavior, and parser argument handling.
  • Bug Fixes

    • Corrected descriptor inheritance, argument validation, interrupt handling, and CPU-count behavior.
    • Strengthened sandbox behavior for host-affecting operations.
  • Tests

    • Expanded parity coverage for operating-system APIs, scheduling, paths, timestamps, string handling, auditing, and XML parsing.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 94f7dd92-75d7-4549-890b-feac98a488bf

📥 Commits

Reviewing files that changed from the base of the PR and between ddad412 and c157a14.

📒 Files selected for processing (10)
  • .github/workflows/pyre-ci.yml
  • pyre/bench/synth/unary_negative.py
  • pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
  • pyre/extra_tests/parity_tests/sys_audit_hooks.py
  • pyre/extra_tests/parity_tests/unary_negative_int_min_jit.py
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-object/src/longobject.rs

Walkthrough

The PR expands POSIX parity coverage and interpreter behavior. It adds argument validation, path-error reporting, platform APIs, CPU-count handling, descriptor and timestamp fixes, EINTR retries, audit and parser validation, surrogate-safe function names, and CI execution after prior check failures.

Changes

POSIX API binding and path conversion

Layer / File(s) Summary
Argument binding and path conversion
pyre/pyre-interpreter/src/gateway.rs, pyre/pyre-interpreter/src/module/posix/interp_posix.rs, pyre/extra_tests/parity_tests/os_arg_binding_surface.py, pyre/extra_tests/parity_tests/os_path_argument_types.py
POSIX APIs enforce declared signatures. Path conversion errors include function and argument names. Tests cover exact errors and side effects.

Platform API support

Layer / File(s) Summary
Platform APIs and processor support
pyre/pyre-interpreter/src/host_seam.rs, pyre/pyre-interpreter/src/module/posix/interp_posix.rs, pyre/pyre-interpreter/src/module/sys/vm.rs, pyre/extra_tests/parity_tests/os_sched_policy_pipe2.py, pyre/extra_tests/parity_tests/os_cpu_count.py
The interpreter adds platform-gated flags, pipe2, scheduling, CPU affinity, processor-based CPU counts, and sandbox stubs.

Descriptor and I/O behavior

Layer / File(s) Summary
Descriptor, timestamp, and transfer behavior
pyre/pyre-interpreter/src/module/posix/interp_posix.rs, pyre/extra_tests/parity_tests/os_dup2_inheritable.py, pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py, .github/workflows/pyre-ci.yml
dup2 supports signature-aware inheritable handling. utime, truncation, and sendfile preserve platform semantics. CI runs parity tests unless cancelled.

Runtime validation

Layer / File(s) Summary
Audit and parser validation
pyre/pyre-interpreter/src/module/sys/vm.rs, pyre/pyre-interpreter/src/module/pyexpat/mod.rs, pyre/extra_tests/parity_tests/audit_and_parsercreate_name_argument.py
sys.audit and ParserCreate validate arguments, types, UTF-8 values, and omitted versus explicit intern settings.

Function name preservation

Layer / File(s) Summary
Surrogate-preserving function names
pyre/pyre-interpreter/src/function.rs, pyre/extra_tests/parity_tests/str_lone_surrogate_names.py
Function constructors preserve lone-surrogate names and the supplied name object.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

A rabbit checks each path and pipe,
While CPU counts stay in type.
Surrogate names safely remain,
And CI tests run through failure rain.
All parity hops in line.

🚥 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 argument-binding changes and the lone-surrogate process-abort fix.
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 rewrite-tracer

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.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit c157a14).
Updated: 2026-08-09T17:05:28.897Z

Files in the reviewed diff
.github/workflows/pyre-ci.yml
pyre/bench/synth/unary_negative.py
pyre/extra_tests/parity_tests/audit_and_parsercreate_name_argument.py
pyre/extra_tests/parity_tests/os_access_modifiers.py
pyre/extra_tests/parity_tests/os_arg_binding_surface.py
pyre/extra_tests/parity_tests/os_cpu_count.py
pyre/extra_tests/parity_tests/os_dup2_inheritable.py
pyre/extra_tests/parity_tests/os_path_argument_types.py
pyre/extra_tests/parity_tests/os_sched_policy_pipe2.py
pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
pyre/extra_tests/parity_tests/str_lone_surrogate_names.py
pyre/extra_tests/parity_tests/sys_audit_hooks.py
pyre/extra_tests/parity_tests/unary_negative_int_min_jit.py
pyre/pyre-interpreter/src/function.rs
pyre/pyre-interpreter/src/gateway.rs
pyre/pyre-interpreter/src/host_seam.rs
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
pyre/pyre-interpreter/src/module/pyexpat/mod.rs
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-object/src/longobject.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-interpreter/src/module/sys/vm.rs:2730 ↔ pypy/module/sys/vm.py:496sys.addaudithook now suppresses every Exception (error_is_exception), whereas PyPy suppresses only a RuntimeError; a hook raising ValueError must propagate in PyPy.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:5690 ↔ pypy/module/posix/interp_posix.py:3096-3100 — successful sched_setscheduler returns None; PyPy returns space.newint(res), i.e. 0.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:5715 ↔ pypy/module/posix/interp_posix.py:3129-3133 — successful sched_setparam returns None; PyPy returns space.newint(res), i.e. 0.

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

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:9403-9405 ↔ rpython/rlib/rposix.py:2981-2984 — Windows cpu_count uses Rust’s available_parallelism() (process-affinity-limited) instead of GetSystemInfo().dwNumberOfProcessors (processor-group count). This behavior predates the patch; the patch only documents it.

4. Structural adaptations

  • pyre/pyre-interpreter/src/function.rs:1951-1957 ↔ pypy/interpreter/function.py:462-465 — Rust’s String cannot represent lone surrogates, so the raw function-name mirror is lossy while w_name preserves the original Python string. This is a Rust UTF-8 storage adaptation.

  • pyre/pyre-interpreter/src/module/pyexpat/mod.rs:1767-1781 ↔ pypy/module/pyexpat/interp_pyexpat.py:924-937 — Pyre rejects lone-surrogate encoding/separator strings before a later Rust &str access; PyPy’s space.text_w uses an RPython UTF-8 string representation. This is a UTF-8 representation adaptation.

  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs:5524-5541 ↔ pypy/objspace/std/longobject.py:283-293 — the JIT explicitly reads Rust BigInt payload fields and calls a raw three-way comparator, rather than invoking PyPy’s rbigint methods on W_LongObject.num. This is the required Rust/JIT IR representation adaptation.

@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

https://github.com/youknowone/pyre/blob/a0d8d952d808e8be1126c64222bf2b532f91f212/pyre-interpreter/src/module/posix/interp_posix.rs#L5463
P1 Badge Reject overflowing PIDs before scheduler syscalls

Convert the PID with a checked pid_t conversion instead of as. On Linux/Android, pid_t is a 32-bit integer, so a call such as os.sched_setaffinity(2**32, mask) wraps the PID to 0 and modifies the current process rather than raising OverflowError; the same unchecked conversion affects the newly added affinity and scheduling-policy functions.


https://github.com/youknowone/pyre/blob/a0d8d952d808e8be1126c64222bf2b532f91f212/pyre-interpreter/src/module/posix/interp_posix.rs#L5366
P1 Badge Preserve the scheduler setter result from PyPy

Return the wrapped syscall result rather than None on success. The referenced PyPy implementations of both sched_setscheduler and sched_setparam return space.newint(res), while rposix.handle_posix_error returns the successful syscall value (0), so direct callers currently observe a deliberate semantic divergence from the required line-by-line port.

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

ℹ️ 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".

@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: 5

🤖 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_path_argument_types.py`:
- Around line 82-110: The expectations for integer path arguments in chdir and
scandir currently rely only on sys.platform; update these conditions to also
require the runtime FD support/build-mode signals used by allow_fd, specifically
HAVE_FCHDIR, HAVE_FDOPENDIR, and the applicable HOST_POSIX/host_env/sandbox
configuration. Ensure Unix builds that reject descriptors use the path-only
error messages.

In `@pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py`:
- Around line 99-107: Update the os.utime descriptor branch to capture a fresh
reference timestamp immediately before os.utime(fd), then assert both st_mtime
and st_atime from os.stat(p) against that reference, matching the existing
path-branch checks.

In `@pyre/extra_tests/parity_tests/str_lone_surrogate_names.py`:
- Around line 46-56: Update the assertions after f.__name__ = SURR and
types.FunctionType construction to verify identity with SURR using is, while
retaining the existing value checks and messages.

In `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs`:
- Around line 7192-7213: Update the access implementation around bind_path_args
to retain and inspect all bound modifier keywords instead of discarding them.
Reject any supplied dir_fd, effective_ids, or follow_symlinks value with the
established unavailable-platform error pattern before calling access(2), while
preserving normal behavior when none are provided.
- Around line 1139-1156: Gate the O_FSYNC entry in the flag definitions to only
Unix targets where libc::O_FSYNC exists, or provide the project’s established
zero/default fallback for unsupported targets. Keep the existing O_FSYNC value
unchanged on supported platforms and preserve the surrounding O_* entries.
🪄 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: 15994c50-5140-4224-ab87-923212dd2cc5

📥 Commits

Reviewing files that changed from the base of the PR and between 59dfaf3 and a0d8d95.

📒 Files selected for processing (13)
  • .github/workflows/pyre-ci.yml
  • pyre/extra_tests/parity_tests/os_arg_binding_surface.py
  • pyre/extra_tests/parity_tests/os_cpu_count.py
  • pyre/extra_tests/parity_tests/os_dup2_inheritable.py
  • pyre/extra_tests/parity_tests/os_path_argument_types.py
  • pyre/extra_tests/parity_tests/os_sched_policy_pipe2.py
  • pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
  • pyre/extra_tests/parity_tests/str_lone_surrogate_names.py
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/gateway.rs
  • pyre/pyre-interpreter/src/host_seam.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs

Comment thread pyre/extra_tests/parity_tests/os_path_argument_types.py
Comment thread pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
Comment on lines +46 to +56
f.__name__ = SURR
check(f.__name__ == SURR, f"__name__ came back as {f.__name__!r}, not the value set")
check(isinstance(repr(f), str), "repr of a surrogate-named function is not a str")

# The qualname slot is separate and must not have been clobbered by the name.
f.__qualname__ = SURR
check(f.__qualname__ == SURR, f"__qualname__ came back as {f.__qualname__!r}")

# ── the constructor arm takes the same name ──────────────────────────────
g = types.FunctionType(f.__code__, {}, SURR)
check(g.__name__ == SURR, f"FunctionType name came back as {g.__name__!r}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the supplied name object identity.

Equality does not detect a reconstructed string object. Add is SURR assertions after both __name__ assignment and types.FunctionType construction. This protects the contract that w_name retains the caller-supplied object.

Proposed test change
 f.__name__ = SURR
 check(f.__name__ == SURR, f"__name__ came back as {f.__name__!r}, not the value set")
+check(f.__name__ is SURR, "__name__ did not retain the assigned string object")
 check(isinstance(repr(f), str), "repr of a surrogate-named function is not a str")
 
 ...
 g = types.FunctionType(f.__code__, {}, SURR)
 check(g.__name__ == SURR, f"FunctionType name came back as {g.__name__!r}")
+check(g.__name__ is SURR, "FunctionType did not retain the supplied string object")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
f.__name__ = SURR
check(f.__name__ == SURR, f"__name__ came back as {f.__name__!r}, not the value set")
check(isinstance(repr(f), str), "repr of a surrogate-named function is not a str")
# The qualname slot is separate and must not have been clobbered by the name.
f.__qualname__ = SURR
check(f.__qualname__ == SURR, f"__qualname__ came back as {f.__qualname__!r}")
# ── the constructor arm takes the same name ──────────────────────────────
g = types.FunctionType(f.__code__, {}, SURR)
check(g.__name__ == SURR, f"FunctionType name came back as {g.__name__!r}")
f.__name__ = SURR
check(f.__name__ == SURR, f"__name__ came back as {f.__name__!r}, not the value set")
check(f.__name__ is SURR, "__name__ did not retain the assigned string object")
check(isinstance(repr(f), str), "repr of a surrogate-named function is not a str")
# The qualname slot is separate and must not have been clobbered by the name.
f.__qualname__ = SURR
check(f.__qualname__ == SURR, f"__qualname__ came back as {f.__qualname__!r}")
# ── the constructor arm takes the same name ──────────────────────────────
g = types.FunctionType(f.__code__, {}, SURR)
check(g.__name__ == SURR, f"FunctionType name came back as {g.__name__!r}")
check(g.__name__ is SURR, "FunctionType did not retain the supplied string object")
🤖 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/str_lone_surrogate_names.py` around lines 46 -
56, Update the assertions after f.__name__ = SURR and types.FunctionType
construction to verify identity with SURR using is, while retaining the existing
value checks and messages.

Comment on lines +1139 to +1156
#[cfg(unix)]
("O_CLOEXEC", libc::O_CLOEXEC as i64),
// The rest of the `<fcntl.h>` set. Each value is the host header's own,
// and the split below is the hosts' own too: these six are on every
// Unix, the next two groups are one platform's each. `nt` has none of
// them and is left with the flags it does have.
#[cfg(unix)]
("O_ACCMODE", libc::O_ACCMODE as i64),
#[cfg(unix)]
("O_ASYNC", libc::O_ASYNC as i64),
#[cfg(unix)]
("O_DIRECTORY", libc::O_DIRECTORY as i64),
#[cfg(unix)]
("O_FSYNC", libc::O_FSYNC as i64),
#[cfg(unix)]
("O_NOCTTY", libc::O_NOCTTY as i64),
#[cfg(unix)]
("O_NOFOLLOW", libc::O_NOFOLLOW 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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

libc crate O_ACCMODE O_ASYNC O_FSYNC O_NOFOLLOW_ANY O_SEARCH which unix targets define

💡 Result:

The constants O_ACCMODE, O_ASYNC, O_FSYNC, O_NOFOLLOW_ANY, and O_SEARCH have varying levels of support and definitions across different Unix targets within the Rust libc crate [1][2]. O_ACCMODE: This constant is widely available across Unix targets in the libc crate [3][4]. It serves as a bitmask for file access mode flags (e.g., O_RDONLY, O_WRONLY, O_RDWR) [5][4]. O_ASYNC: This constant is generally available on most Linux-based targets [3][4]. It is typically associated with asynchronous I/O and is commonly found in libc definitions for platforms like Linux (GNU and other environments) [6][4]. It is often explicitly excluded from certain platforms like AIX, Solaris, Haiku, or Cygwin [3]. O_FSYNC: This constant is less universally defined under this specific name in libc [3]. It is often an alias for O_SYNC on many Linux platforms [7]. In the libc crate, it is available on select Unix-like targets, including various BSD and Linux configurations [3]. O_SEARCH: This constant, defined by POSIX to open a directory for searching, has historically been absent from many systems' libc headers because Linux and other systems often lack native support or use different mechanisms (like O_PATH) [8][9]. Following community efforts, it has been added to the libc crate for various supported platforms [10]. Its presence depends on the specific target's libc implementation [10]. O_NOFOLLOW_ANY: This is not a standard POSIX flag and is not broadly supported across Unix-like targets in the libc crate [11]. It is primarily associated with macOS (Darwin) [11][4]. It is used to prevent following symbolic links throughout an entire path [12]. Because it is a platform-specific flag, it is not consistently exposed in the libc crate for non-macOS targets [11][4]. For specific target compatibility, users should consult the libc crate documentation for the target in question, as definitions are generated based on the underlying system's headers [2][13]. Code relying on these constants should generally use conditional compilation (#[cfg(...)]) if cross-platform support is required [11].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo file excerpt:"
sed -n '1100,1195p' pyre/pyre-interpreter/src/module/posix/interp_posix.rs

echo
echo "Cargo libc version/context:"
rg -n '^\[dependencies\]|\blibc\b|^\[dev-dependencies\]|\blibc\b|^\[build-dependencies\]|\blibc\b' Cargo.toml pyre/pyre-interpreter/Cargo.toml 2>/dev/null || true

echo
echo "Search cfg guards around fcntl constants:"
rg -n '#\[cfg|O_CLOEXEC|O_ACCMODE|O_ASYNC|O_DIRECTORY|O_FSYNC|O_NOCTTY|O_NOFOLLOW|O_NOFOLLOW_ANY|O_EXEC|O_SEARCH|O_DIRECT' pyre/pyre-interpreter/src/module/posix/interp_posix.rs

Repository: youknowone/pyre

Length of output: 20710


🌐 Web query:

latest libc crate generated docs FSync Haiku O_FSYNC

💡 Result:

In the Rust libc crate, the O_FSYNC constant is not defined for the Haiku target [1]. Haiku's native headers define flags such as O_SYNC, O_RSYNC, and O_DSYNC [2], but O_FSYNC is generally associated with other POSIX-like systems (such as Linux or various BSD variants) where it is often an alias for O_SYNC [3][4]. While the libc crate provides raw FFI bindings for many platforms, it does not expose O_FSYNC for Haiku, reflecting the underlying POSIX implementation details and header definitions of the Haiku operating system [1][2]. High-level crates like nix also explicitly exclude Haiku when defining O_FSYNC to ensure compatibility with the target platform's supported flags [5][6].

Citations:


Gate O_FSYNC to targets that define it.

#[cfg(unix)] ("O_FSYNC", libc::O_FSYNC as i64) cannot compile on Unix targets that do not provide libc::O_FSYNC; Haiku is one such target. Add a narrower cfg path for this flag or publish a zero/default fallback like the other non-universal flags.

🤖 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/posix/interp_posix.rs` around lines 1139 -
1156, Gate the O_FSYNC entry in the flag definitions to only Unix targets where
libc::O_FSYNC exists, or provide the project’s established zero/default fallback
for unsupported targets. Keep the existing O_FSYNC value unchanged on supported
platforms and preserve the surrounding O_* entries.

Comment thread pyre/pyre-interpreter/src/module/posix/interp_posix.rs
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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

♻️ Duplicate comments (2)
pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py (1)

150-158: 🎯 Functional Correctness | 🟡 Minor

Add the missing descriptor access-time assertion.

Line 157 checks only st_mtime and reuses before from the path branch. The test does not detect an implementation that leaves st_atime at OLD. Capture a descriptor-specific reference immediately before os.utime(fd) and assert both timestamps from one os.stat(p) result. This finding remains unresolved from the previous review.

Proposed test fix
     fd = os.open(p, os.O_RDWR)
     try:
+        descriptor_before = time.time()
         os.utime(fd)
     finally:
         os.close(fd)
-    check(abs(os.stat(p).st_mtime - before) < 60, f"utime(fd) -> {os.stat(p).st_mtime}")
+    stat_result = os.stat(p)
+    check(
+        abs(stat_result.st_mtime - descriptor_before) < 60,
+        f"utime(fd) -> {stat_result.st_mtime}",
+    )
+    check(
+        abs(stat_result.st_atime - descriptor_before) < 60,
+        "utime(fd) left the access time behind",
+    )
🤖 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/os_utime_pathconf_truncate.py` around lines 150
- 158, Update the os.utime descriptor test to capture a descriptor-specific
timestamp immediately before os.utime(fd), then perform one os.stat(p) and
assert both st_mtime and st_atime against that reference. Do not reuse the
path-branch before value, and preserve the existing descriptor setup and
cleanup.
pyre/pyre-interpreter/src/module/posix/interp_posix.rs (1)

1158-1198: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

O_FSYNC and O_ASYNC are published under a target-wide unix gate. Both files name these two libc constants without a per-target gate, but libc does not define them for every Unix target (Haiku lacks O_FSYNC; AIX, Solaris and Cygwin lack O_ASYNC). The build then fails on those targets.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs#L1158-L1198: replace the #[cfg(unix)] gate on the O_FSYNC and O_ASYNC entries with the narrower target list used by the Linux and Apple groups below them, or omit the name where the header has none.
  • pyre/pyre-interpreter/src/host_seam.rs#L77-L83: move O_ASYNC and O_FSYNC out of the unconditional pub use list into a gated re-export that matches the interp_posix gate.
🤖 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/posix/interp_posix.rs` around lines 1158 -
1198, Restrict the O_ASYNC and O_FSYNC entries in interp_posix.rs to the same
supported Linux/Apple target gate used by the surrounding platform-specific
constants, or omit unsupported names. In host_seam.rs, remove both from the
unconditional pub use list and add a matching gated re-export so both files
compile consistently on targets where libc lacks these constants.
🤖 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/audit_and_parsercreate_name_argument.py`:
- Around line 167-168: Fix the incomplete comment near the ParserCreate checks
by aligning its wording with the intended sentence form used near line 111,
while preserving the existing meaning about accepting calls and ParserCreate no
longer building parsers.

In `@pyre/extra_tests/parity_tests/os_cpu_count.py`:
- Around line 65-76: Update the CPU-count validation around host_cpu_count to
avoid equating its sysctl-based result with SC_NPROCESSORS_ONLN on Apple/BSD
platforms; import sys for the platform check, use SC_NPROCESSORS_CONF for those
platforms, and retain the online-count comparison elsewhere.

In `@pyre/extra_tests/parity_tests/os_path_argument_types.py`:
- Around line 103-110: Update the parity expectations for truncate, statvfs,
chown, and pathconf to account for host-gated allow_fd behavior: use the
path-only expectation when their corresponding HAVE_FTRUNCATE, HAVE_FSTATVFS,
HAVE_FCHOWN, or HAVE_FPATHCONF feature is unavailable, while retaining WITH_FD
when enabled. Handle utime separately using its HAVE_FUTIMENS/platform behavior,
following the existing listdir/scandir guard pattern where appropriate.

In `@pyre/pyre-interpreter/src/function.rs`:
- Around line 1951-1956: Update name_utf8_mirror and the related
constructor/repr paths to preserve Python-visible surrogate names by reusing the
retained w_name object instead of converting Wtf8 through to_string_lossy().
Keep the existing valid-UTF-8 path unchanged, and ensure lone surrogates remain
represented exactly as stored in w_name.

---

Duplicate comments:
In `@pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py`:
- Around line 150-158: Update the os.utime descriptor test to capture a
descriptor-specific timestamp immediately before os.utime(fd), then perform one
os.stat(p) and assert both st_mtime and st_atime against that reference. Do not
reuse the path-branch before value, and preserve the existing descriptor setup
and cleanup.

In `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs`:
- Around line 1158-1198: Restrict the O_ASYNC and O_FSYNC entries in
interp_posix.rs to the same supported Linux/Apple target gate used by the
surrounding platform-specific constants, or omit unsupported names. In
host_seam.rs, remove both from the unconditional pub use list and add a matching
gated re-export so both files compile consistently on targets where libc lacks
these constants.
🪄 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: f8eeecb7-ce29-495e-a55f-784a4cbad6f9

📥 Commits

Reviewing files that changed from the base of the PR and between eba36d1 and ddad412.

📒 Files selected for processing (16)
  • .github/workflows/pyre-ci.yml
  • pyre/extra_tests/parity_tests/audit_and_parsercreate_name_argument.py
  • pyre/extra_tests/parity_tests/os_access_modifiers.py
  • pyre/extra_tests/parity_tests/os_arg_binding_surface.py
  • pyre/extra_tests/parity_tests/os_cpu_count.py
  • pyre/extra_tests/parity_tests/os_dup2_inheritable.py
  • pyre/extra_tests/parity_tests/os_path_argument_types.py
  • pyre/extra_tests/parity_tests/os_sched_policy_pipe2.py
  • pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
  • pyre/extra_tests/parity_tests/str_lone_surrogate_names.py
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/gateway.rs
  • pyre/pyre-interpreter/src/host_seam.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-interpreter/src/module/pyexpat/mod.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs

Comment on lines +167 to +168
# The accepting calls, so the checks above are not passing because
# ParserCreate stopped building parsers.

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 | 🟡 Minor | ⚡ Quick win

Fix the comment sentence.

The sentence is incomplete. Line 111 uses the intended form. Align this one with it.

📝 Proposed wording fix
-# The accepting calls, so the checks above are not passing because
-# ParserCreate stopped building parsers.
+# ...and the accepting calls still accept, so the checks above are not passing
+# because ParserCreate stopped building parsers.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# The accepting calls, so the checks above are not passing because
# ParserCreate stopped building parsers.
# ...and the accepting calls still accept, so the checks above are not passing
# because ParserCreate stopped building parsers.
🤖 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/audit_and_parsercreate_name_argument.py` around
lines 167 - 168, Fix the incomplete comment near the ParserCreate checks by
aligning its wording with the intended sentence form used near line 111, while
preserving the existing meaning about accepting calls and ParserCreate no longer
building parsers.

Comment on lines +65 to +76
# ── the value, not just its stability ────────────────────────────────────
# A constant wrong answer would satisfy everything above. The processor count
# the host reports through sysconf is the one both sides are built on — the
# `sysconf(_SC_NPROCESSORS_ONLN)` arm directly, the `sysctl(CTL_HW, HW_NCPU)`
# arm because a host reports the same processors either way.
if before is not None and hasattr(os, "sysconf"):
try:
onln = os.sysconf("SC_NPROCESSORS_ONLN")
except (ValueError, OSError):
onln = None
if onln is not None and onln > 0:
check(before == onln, f"cpu_count {before} is not the host's {onln} processors")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

HW_NCPU and _SC_NPROCESSORS_ONLN can disagree.

The Apple and BSD arm of host_cpu_count reads sysctl(CTL_HW, HW_NCPU), which reports the configured processor count. _SC_NPROCESSORS_ONLN reports the online count. On a host with a processor offline the two differ and this equality check fails. Compare against SC_NPROCESSORS_CONF on those platforms, or relax the check to an upper bound.

🔧 Proposed fix
-if before is not None and hasattr(os, "sysconf"):
-    try:
-        onln = os.sysconf("SC_NPROCESSORS_ONLN")
-    except (ValueError, OSError):
-        onln = None
-    if onln is not None and onln > 0:
-        check(before == onln, f"cpu_count {before} is not the host's {onln} processors")
+if before is not None and hasattr(os, "sysconf"):
+    # The sysctl(CTL_HW, HW_NCPU) arm reports the configured count, so the
+    # name to compare against differs from the sysconf(_SC_NPROCESSORS_ONLN)
+    # arm's.
+    name = "SC_NPROCESSORS_CONF" if sys.platform == "darwin" else "SC_NPROCESSORS_ONLN"
+    try:
+        reported = os.sysconf(name)
+    except (ValueError, OSError):
+        reported = None
+    if reported is not None and reported > 0:
+        check(before == reported, f"cpu_count {before} is not the host's {reported} processors")

sys needs importing for the platform test.

🤖 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/os_cpu_count.py` around lines 65 - 76, Update
the CPU-count validation around host_cpu_count to avoid equating its
sysctl-based result with SC_NPROCESSORS_ONLN on Apple/BSD platforms; import sys
for the platform check, use SC_NPROCESSORS_CONF for those platforms, and retain
the online-count comparison elsewhere.

Comment thread pyre/extra_tests/parity_tests/os_path_argument_types.py
Comment thread pyre/pyre-interpreter/src/function.rs

@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: 918e0ddac7

ℹ️ 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 +5667 to +5668
#[cfg(not(target_env = "musl"))]
{

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 Publish scheduler setters on musl

On Linux/musl, this condition newly publishes sched_getparam, sched_getscheduler, sched_param, and sched_rr_get_interval while omitting the matching setters, even though musl provides the POSIX sched_setparam and sched_setscheduler calls. This also violates the upstream grouping in pypy/module/posix/moduledef.py, where the presence of sched_getparam implies that sched_setparam is exported; applications that use the getter/type as the capability probe will therefore fail only when they attempt the write. Remove the musl exclusion (or gate the entire group consistently if the wrapper genuinely cannot support it).

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

Useful? React with 👍 / 👎.

… retry through the signal gate

`utime_impl` read the current time out of `SystemTime::now()` when the caller
named neither `times` nor `ns`, and passed that pair to `futimens` and
`utimensat`. `parse_utime_args` (interp_posix.py:1918-1946) answers a `now`
flag beside the pair instead, and `do_utimens` (:1948-1953) writes
`rposix.UTIME_NOW` over both nanosecond fields. The flag travels the same way
here now: `timespec_of` takes it for the two POSIX calls, and the Windows
`SetFileTime` branch, which has no word for "now", reads the clock itself.

`ftruncate_retry` compared the errno against EINTR and went straight back to
the call. `crt_call!` (builtins.rs:6243-6253) is the raw-call wrapper, not the
call gate, so no Python signal handler ran between the retries;
interp_posix.py:404-412 retries under `wrap_oserror(..., eintr_retry=True)`.
It goes through `builtins::eintr_retry_with` now, as the `open` loop beside it
already did, and takes its OSError wrapper from the callers, so `os.truncate`
still names the file it opened and `os.ftruncate` still names none.

os_utime_pathconf_truncate.py covers the unnamed-time form by name and by
descriptor.

The comment above `sendfile` said pyre's os wrappers do no manual EINTR retry;
three of them do. It states the divergence from interp_posix.py:2958-2974
instead.

Assisted-by: Claude
The step carried no condition, so a red check.py skipped it. On this branch the
parity suite last ran in run 31131072279; the two runs after it went red on
jitstats rows and the step reported nothing on any of the three platforms —
including the thirteen Windows failures that earlier run had surfaced. The job
still fails on either gate.

Assisted-by: Claude
`path_converter` fills `function_name` and `argument_name` from the argument
clinic, so a rejected path reports "{func}: {arg} should be {types}, not {tp}".
`path_or_fd_w` took a function name alone and spelled every argument "path". It
now takes the pair, and `fsencode_path_named_w` gives a path-only boundary both
halves; `fsencode_path_or_fd_w` and its nullable twin pass "path", so the
messages they already emitted are unchanged.

Named: readlink, rename and replace (src/dst, after whichever of the two the
caller reached, since they share one body), symlink, link, access, chroot,
execv, execve, posix_spawn and posix_spawnp (likewise name-derived), and the
Windows chdir, access, link and symlink arms. The Windows twins take the same
strings as their POSIX counterparts; only chdir's allowed-type list differs,
because there is no fchdir there to put "integer" in it.

Left with the caller-less conversion, each with the reason recorded in place:
the Windows-only boundaries (the _get*name family, startfile, listmounts,
_path_splitroot, and the Windows `system`, which declares text rather than a
path and so has a different message shape entirely), whose wording cannot be
measured from a POSIX host; and the values a sequence or mapping is walked for
-- argv entries, execve environment values, posix_spawn file actions -- which
answer the caller-less message under 3.14 as well.

Measured by emitting the TypeError for every (os entry point, argument
position) under CPython 3.14 and under pyre and diffing the two: 48 lines, no
divergence after the change. That measurement is also what found `access`,
`execv` and `execve`, which reach the converter through `extract_path` rather
than through `fsencode_path_w`.

os_path_argument_types.py drops its UNPINNED category and asserts the measured
message at every boundary, including each half of the two-path pairs and the
positions where caller-less is the correct answer.

Assisted-by: Claude
`interp_posix.py:2958-2974` runs both arms inside
`while True: ... except OSError: wrap_oserror(..., eintr_retry=True)`, so an
interrupted transfer runs the pending Python signal handlers and goes back to
the call. The three call sites here had no loop and surfaced InterruptedError
instead; they now retry through `builtins::eintr_retry_with`, as `ftruncate`,
`truncate`'s `open` and `lockf` already do.

The BSD arm keeps its partial-transfer rescue exactly as it was.
`rposix.py:3086-3095` returns `sbytes` for `EAGAIN` and `EBUSY` alone; EINTR is
not in that set and falls through to `handle_posix_error`, which raises, and the
loop above it then re-runs the whole call with the same `offset` and `count` --
both are loop-invariant in `interp_posix.py` and `rposix` never sees the retry.
So a partial transfer a signal interrupted is discarded and the range the caller
asked for is requested again.

The Linux offset arm re-seeds `offset` from the caller's value on every attempt
for the same reason: `rposix.sendfile` (`rposix.py:3061-3065`) writes the offset
into a fresh cell each call from the argument it was passed, so what a failed
call left in the out-parameter is not what the next one starts from.

Both Linux arms are `#[cfg(target_os = "linux")]` and do not compile on this
host, and `cargo check --target x86_64-unknown-linux-gnu` cannot build them
either (cc-rs: failed to find tool "x86_64-linux-gnu-gcc"). They were checked on
a real Linux box instead -- `cargo check -p pyre-interpreter --features dynasm`
in a Debian trixie container, rc=0, no new warnings. macOS: the same check plus
`--features dynasm,sandbox`, both rc=0.

Assisted-by: Claude
…them

Registers `pipe2`, `sched_rr_get_interval`, `sched_getscheduler`,
`sched_setscheduler`, `sched_getparam`, `sched_setparam` and the
`posix.sched_param` structseq, each gated to the targets whose libc
declares it (`moduledef.py:166-174`, `:216`). The setters are left out
under musl, which declares neither.

Both setters answer None; `interp_posix.py:3097`/`:3131` hand back the raw
`handle_posix_error` result, which is 0 on every success. A `w_param` that
is not a `sched_param` is refused with "must have a sched_param object",
and a priority the C `int` cannot hold with "sched_priority out of range"
(`interp_posix.py:3086-3092`).

Adds `O_CLOEXEC` to the constants table. `moduledef.py:264-266` publishes
it by name and `pipe2` takes it as its flag; `nt` spells the same intent
O_NOINHERIT, so the entry is Unix-only.

`host_seam::sys` gains the `sched_param` type and `O_CLOEXEC`. The seam
re-exports no syscall function, so `sched_rr_get_interval` — the one call
here that reaches libc directly rather than through host_env — is not
compiled into a sandbox build; the raising stub serves the name there.

Drops "dup3" from the sandbox denial list: neither `moduledef.py` nor `os`
publishes such a name on any host. Moves "pipe2" and the sched_* names out
of the unconditional list into target-gated loops, since `module_ns_store`
writes rather than overwrites and would otherwise publish a name the build
never registered.

Adds pyre/extra_tests/parity_tests/os_sched_policy_pipe2.py.

Assisted-by: Claude
sched_getaffinity/sched_setaffinity are registered on linux and android
outside a sandbox build, written against the host header: moduledef.py
names neither and rposix wraps neither. The mask is the fixed cpu_set_t,
CPU_SETSIZE wide, because the libc crate exposes no CPU_ALLOC; a CPU
number at or past that width is refused with EINVAL. Both names are
served by sandbox_unavailable in a sandbox build.

Eighteen further O_* constants join O_CLOEXEC in the table, in three
groups: six on every unix, six on linux/android, seven on the Apple
targets. host_seam::sys re-exports all of them, the two platform groups
as separate gated blocks.

sys._get_cpu_count_config() answers -1. os.py:1180 reads it to decide
whether process_cpu_count counts the affinity mask or aliases cpu_count,
so publishing sched_getaffinity without it makes `import os` raise. The
value is set by -X cpu_count and PYTHON_CPU_COUNT, neither of which this
interpreter reads.

The sandbox denial list's sched_getscheduler/sched_getparam/
sched_rr_get_interval loop keeps the gate it was written with; the
affinity pair is stored after it under its own.

os_sched_policy_pipe2.py covers the flag set through a per-platform
table and the affinity pair through its mask spellings and refusals.

Assisted-by: Claude
… count

os.cpu_count and posix._cpu_count answered
host_posix::get_number_of_os_threads(), which reads /proc/self/stat's
num_threads on linux and the mach task_threads count on the Apple
targets. That is the quantity warn_if_multi_threaded uses in the fork
path, so both names reported how many threads happened to be alive: 2 on
a 9-processor linux host and 2 on an 18-processor macOS host, and the
value moved to 8 and back while six threads ran.

host_cpu_count ports rpy_cpu_count's two unix arms — sysconf
(_SC_NPROCESSORS_ONLN) on linux and android, sysctl(CTL_HW, HW_NCPU) on
the Apple and BSD targets, 0 elsewhere — and the registrations keep the
count <= 0 answer of None. Both now report 9 and 18 on those hosts.

The two registrations carry #[cfg(not(feature = "sandbox"))] because the
helper makes a syscall; the sandbox denial list already served both
names.

The windows registration is unchanged and carries a comment recording
that rposix.py:2978-2986 reads GetSystemInfo().dwNumberOfProcessors
where it calls available_parallelism, and that no windows oracle is
reachable from this host to measure which the surface should report.

os_cpu_count.py asserts the count does not move while six threads are
started and joined, and that it is the processor count sysconf reports.

Assisted-by: Claude
Both dup2 registrations — the unix arm and the nt arm — were
make_builtin_function with no Signature, so the trailing __pyre_kw__
marker dict was never split off the argument slice. It occupied the
third positional slot and read truthy, so dup2(fd, fd2,
inheritable=False) returned an inheritable descriptor; an unknown
keyword, a fifth positional, and a keyword duplicating a positional
were all accepted, and dup2(fd, fd2=n) reached c_int_w with the dict.

Converts both to #[pyre_function] with
make_builtin_function_with_arity_and_maybe_sig. The parameters stay
PyObjectRef and are unwrapped in the body with c_int_w / is_true: the
macro's i32 binding is a w_int_get_value cast that performs no type
check.

Adds pyre/extra_tests/parity_tests/os_dup2_inheritable.py, asserting
get_inheritable after both settings and the default, binding by name,
and the exception type of six binding errors. It does not assert
message text — dup2 is keyword-bindable upstream, so its arity text
belongs to the clinic family, and #[pyre_function] emits the
positional-only family until the #[posonly] marker exists.

Assisted-by: Claude
access, get_terminal_size, listdir, posix_spawn, posix_spawnp, readlink,
scandir, sendfile and symlink took their arguments straight off the
argument slice. The trailing __pyre_kw__ marker dict was never split
away, so a keyword either vanished or arrived as a positional value, and
a surplus positional was dropped rather than refused. Of twenty measured
calls, fifteen disagreed with CPython 3.14; symlink(src, dst,
target_is_directory, dir_fd) created the link and returned None.

Routes them through bind_path_args, which already carries the argument
clinic spellings. Adds bind_posonly_args for the two shapes it does not
cover: a parameter list wholly before the `/`, which is parsed by
_PyArg_CheckPositional and refuses every keyword, and a positional-only
prefix with a keyword-only tail, which is parsed by
_PyArg_UnpackKeywords and reports its count in the parenthesised form.

readlink and symlink now name dir_fd and refuse a descriptor for it,
matching what os.supports_dir_fd advertises. access names its three
keyword-only modifiers so that an unknown keyword is an error; it still
calls access(2) rather than faccessat(2), so none of the three is
answered. sendfile names headers, trailers and flags, which neither arm
passes to the syscall.

Adds pyre/extra_tests/parity_tests/os_arg_binding_surface.py. It asserts
message text, not just the exception type, because the two count
spellings are not interchangeable, and it checks that symlink refused
without creating the link.

Assisted-by: Claude
Setting __name__ to a str holding a lone surrogate aborted the process:
function_set_func_name mirrored the name into the raw slot through
w_str_get_value, which panics at unicodeobject.rs:579 when the backing
WTF-8 buffer has no &str view. Such a str is reachable from ordinary
code, since every operating-system name is decoded with surrogateescape
and sys.argv carries one on this host.

Adds name_utf8_mirror, which falls back to the lossy rendering, and uses
it at the setter and at both constructor arms.

That mirror is a UTF-8 String (NameStorage), so it cannot hold the value
exactly, and types.FunctionType(code, globals, name) returned an escaped
name once the abort was gone. Both constructor arms now also store the
supplied object with function_set_name_obj; __name__ reads w_name, so it
returns what was passed, and the mirror serves only repr and the
__qualname__ default.

Found by running 108 operations against a surrogate supplied through
argv and through a non-UTF-8 filename on Linux; this was the only abort.
macOS refuses such a filename with EILSEQ, so that producer was
exercised in a container.

Adds pyre/extra_tests/parity_tests/str_lone_surrogate_names.py, which
builds the surrogate from bytes, asserts the payload before using it,
and asserts round-trip equality rather than survival.

Assisted-by: Claude
`sys.audit` was `|_| Ok(w_none())`, so it accepted anything: no event at all,
an `int`, a `str` with no UTF-8 spelling, or the event passed by keyword.
Give it the checks `@unwrap_spec(event="text")` performs at vm.py:473 —
`audit expected at least 1 argument, got 0`,
`audit() argument 1 must be str, not <type>` (`None` for the singleton),
`sys.audit() takes no keyword arguments`, and `str_utf8_w` for the encoding.
The body past the unwrap still does nothing: `addaudithook` stores no hook.

`ParserCreate` checked that `encoding` is `str` or `None` but stored it
without asking whether it has a UTF-8 spelling, which the sibling
`namespace_separator` arm does ask; add that check. Both arms now report the
argument's own type name through `arg_type_name`; `namespace_separator`
previously spelled it as the literal `int`.

Adds parity test audit_and_parsercreate_name_argument.py, which also pins
`ctypes.CDLL`: a surrogate-escaped library name reaches dlopen and fails
there, while a surrogate that is not an escape is refused before the call.

check.py --backend dynasm,cranelift,wasm: 410/410, 410/410, 406/406.
Parity suite: 243 files, no pyre-side failure.

Assisted-by: Claude
`access` bound `dir_fd`, `effective_ids` and `follow_symlinks` and then called
`access(2)`, which takes none of them: a `dir_fd` was accepted and ignored, so a
relative name resolved against the working directory, and `follow_symlinks=False`
answered about the file a symlink points at. Neither raised.

Port interp_posix.py:747-789. The modified call goes through `faccessat`, with
`AT_SYMLINK_NOFOLLOW` and `AT_EACCESS` per rposix.py:2551-2560; the unmodified
call stays on `access`. Both answer `error == 0` without `handle_posix_error`
upstream, so a refusal is False rather than an OSError. `HAVE_FACCESSAT` joins
`_have_functions`, which is what os.py:117,137,158 read to put `access` in
`supports_dir_fd`, `supports_effective_ids` and `supports_follow_symlinks`.

Where the bit is off — under sandbox — `dir_fd` reports through `DirFD` and the
two flags through the new `argument_unavailable`, matching interp_posix.py:771-775
and :298-301. The chmod site now shares that helper.

Two further changes fall out of the call swap:

* The old body narrowed the mode to `u8` and answered False when it did not fit,
  citing an EINVAL that this platform's `access(2)` does not raise:
  `os.access(f, 256)` answered False where the oracle answers True. The mode is
  now passed as the `c_int` interp_posix.py:744 declares.
* The parameters are converted in declaration order, since each can raise and
  the first one to do so is what the caller sees. Writing the three new
  conversions ahead of `mode` had made `os.access(f, 2**40, dir_fd="s")` report
  the dir_fd, and `os.access(f, 2**40, effective_ids=<raising __bool__>)` run
  `__bool__` at all.

Of thirteen measured calls, five disagreed with python3.14 (three capability-set
memberships besides). All agree now.

Adds parity test os_access_modifiers.py, marked linux,darwin: the assertions are
about `faccessat`, which Windows has not, so the reference fails there too. Its
discriminating pair is the same relative name answering True with `dir_fd` and
False without — a test of the `dir_fd` form alone passes on the old build for
any name that also resolves from the working directory.

check.py --backend dynasm,cranelift,wasm: 410/410, 410/410, 406/406.
Parity suite: 244 files, no pyre-side failure.

Assisted-by: Claude
…t None

`ParserCreate`'s `intern` parameter defaulted to `w_none()`, so an omitted
argument and an explicit `None` arrived identically and neither wrote the
parser's `intern` slot -- the dictionary `init_parser_slots` installs stayed in
place either way, and a caller asking for no interning got names interned.

The default is now the absent marker `PY_NULL`, and `parser_create3` writes the
slot only when the argument was supplied. `intern_string` already reads a
stored `None` back as "do not intern".

Extends the parity test with six rows: the omitted case yields a dictionary and
interns two names, the explicit `None` case yields `None` and interns nothing,
and a caller-supplied dictionary is the one used and filled.

Assisted-by: Claude
`parser_create3` and `sys_audit` both take their name argument through
`str_utf8_w`, which refuses a lone surrogate, where `space.text_w` upstream does
not. Neither site said why.

For pyexpat the reason is a reader: both stored names come back out through
`w_str_get_value`, which panics on a lone surrogate --
`declared_or_forced_encoding` for the encoding, `namespace_separator` for the
separator. The refusal at the door is what keeps those reads from aborting the
process. The `namespace_separator` arm carried no comment at all; the encoding
arm's pointed at the separator arm rather than at the reader.

For `sys.audit` nothing stores the event, so the encode exists only for the
error it raises; the doc comment now says so.

Comment-only.

Assisted-by: Claude
sched_rr_get_interval, sched_getscheduler, sched_getparam,
sched_setscheduler, sched_setparam, sched_getaffinity and
sched_setaffinity took the pid with int_w and cast the result to
libc::pid_t, truncating instead of raising OverflowError.

Assisted-by: Claude
…compare longs by payload

try_walker_specialize_unary_negative_int declined an INT_MIN operand and
left it to the CallMayForce residual. It now emits GUARD_VALUE on the
operand and takes the same _make_ovf2long tail the BINARY_OP overflow
arm takes. That tail moved out of try_walker_specialize_binary_op_int
into walker_emit_ovf2long_box and both callers share it.

The three unary int folds (+, -, ~) gated only on
is_exact_builtin_instance, which settles the recorded operand alone: an
int subclass keeps the builtin ob_type, so the GUARD_CLASS INT they emit
let one enter the trace later and be answered by the fold instead of its
own __pos__ / __neg__ / __invert__. They now emit
walker_guard_exact_w_class as the long folds already did.
walker_unary_int_operand is the shared gate for all three and returns
the class; the positive fold's duplicate inline gate is gone.

try_walker_specialize_compare_op_long passed the two W_LongObject boxes
to jit_w_long_cmp, which read both value payloads inside the callee. It
now records the two getfield_gc_r and calls jit_bigint_cmp on the
payloads. jit_w_long_cmp had no remaining callers and is deleted.

extra_tests/parity_tests/unary_negative_int_min_jit.py covers both
directions of the promote split, a bool operand, and a subclass arriving
through data after the loop is hot.

Assisted-by: Claude

@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: 6f793c2c76

ℹ️ 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 +1954 to +1956
None => unsafe { pyre_object::w_str_get_wtf8(w_name) }
.to_string_lossy()
.into_owned(),

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 Preserve lone surrogates in the function-name mirror

When f.__name__ contains a lone surrogate, to_string_lossy() replaces it with U+FFFD in the raw name slot. That slot remains observable: frame_into_generator_for_function passes function_get_name(function) into the generator, so calling a surrogate-named generator function makes generator.__name__ contain replacement characters instead of the assigned name (and other diagnostics reading this slot are similarly corrupted). Preserve the WTF-8 name for these consumers rather than keeping a lossy UTF-8 copy.

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

Useful? React with 👍 / 👎.

…t event

A hook that raises out of the `sys.addaudithook` event means the set
refused the new hook. error_is_runtime_error tested for RuntimeError,
which is what the C-level PySys_AddAuditHook reads; the sys.addaudithook
a Python caller reaches takes any Exception. Measured against 3.14 with
a refusing hook already installed, RuntimeError, ValueError and
Exception all leave the call returning None and only a BaseException
outside Exception comes back out. Renamed to error_is_exception.

sys_audit handed the hooks the event object it was given. The
`@unwrap_spec(event="text")` round trip re-wraps the unwrapped name, so
a str subclass reaches the hooks flattened to a plain str; it now passes
w_str_new(event) instead.

The addaudithook comment cited vm.py:485; `def addaudithook` is at 486.

extra_tests/parity_tests/sys_audit_hooks.py pins the dispatch, the
argument tuple, the install order, that a new hook does not see its own
installation, the three refusal outcomes, and that an exception from a
hook propagates.

Assisted-by: Claude

@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: c157a14dc2

ℹ️ 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 2729 to 2731
if let Err(err) = audit("sys.addaudithook", &[]) {
if !error_is_runtime_error(&err) {
if !error_is_exception(&err) {
return Err(err);

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 Propagate non-RuntimeError audit-hook failures

When an existing hook raises ValueError or another non-RuntimeError during sys.addaudithook, this predicate now suppresses the exception and silently refuses the new hook. The ported implementation in pypy/module/sys/vm.py:493-499 suppresses only exceptions matching RuntimeError and re-raises everything else, so widening the check to all Exception subclasses changes observable control flow for hook installers; retain the RuntimeError-specific predicate.

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

Useful? React with 👍 / 👎.

Comment on lines +7748 to +7750
&[
"out_fd", "in_fd", "offset", "count", "headers", "trailers", "flags",
],

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 the four-argument sendfile signature

When callers provide headers, trailers, flags, or any fifth through seventh positional argument, this parameter list binds them successfully even though the implementation never reads bound[4..]. Such calls therefore perform a normal transfer while silently discarding requested behavior; both the ported function at pypy/module/posix/interp_posix.py:2946-2957 and the supported Linux surface accept only the four common arguments, so these extras must be rejected until their behavior is implemented.

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

Useful? React with 👍 / 👎.

Comment on lines +5688 to +5690
host_posix::sched_setscheduler(pid, policy, &param)
.map_err(|e| io_err(e, ""))?;
Ok(pyre_object::w_none())

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 Return the upstream scheduler status from setters

On every successful sched_setscheduler call this returns None, and the matching sched_setparam body does the same, whereas pypy/module/posix/interp_posix.py:3095-3100 and 3128-3133 return space.newint(res) (normally 0). Successful no-op updates using the current policy and priority make this readily observable, so code checking the upstream status receives a different value; return the host result instead of discarding it.

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

Useful? React with 👍 / 👎.

@youknowone
youknowone merged commit a3bdf03 into main Aug 10, 2026
18 of 22 checks passed
@youknowone
youknowone deleted the rewrite-tracer branch August 10, 2026 00:12
youknowone added a commit that referenced this pull request Aug 13, 2026
… jit: pop-fold guard order and recorded-raise roots; typedef: __init_subclass__ keywords (#1204)

* jit: read the recorded-raise roots back out of their shadow slots

`walker_emit_recorded_builtin_raise` pinned `exc` and each argument and then
kept using the locals it had handed to `pin_root`. `RootScope::pin_root`
normalizes the address it publishes once a second mutator has existed
(`gc_roots.rs`), so past that point the slot and the caller's copy can name
different objects, and these values are baked into the trace as `ConstPtr`s
that outlive the walk.

`args_storage` carried no root at all: it was read off `exc` before the
normalization could apply and then indexed once per argument.

Take every value back from `shadow_stack_get` after pinning it, and pin
`args_storage`, which is the shape the concrete-shadow build for
zip/tuple in this file already uses.

Assisted-by: Claude

* typedef: refuse __init_subclass__ keywords the way parse_obj does

The default `__init_subclass__` reported a leftover class-definition keyword
as `ArgErr::UnknownKwds`, so `class C(Base, flag=1)` raised "got an
unexpected keyword argument 'flag'". `parse_obj` does not report the keyword
when the signature has neither `**kwargs` nor a keyword-only argument
(`argument.py:377-380`); it collapses every such refusal to "takes no
keyword arguments". This signature is `cls` alone, so that branch always
applies. CPython 3.14 raises the same sentence.

The qualname ahead of the `()` is left as it was: pyre spells it
`object.__init_subclass__` and CPython spells it with the subclass, and that
choice is not what this changes.

The snippet asserted only that the message mentioned `__init_subclass__`,
which held for both shapes; it now pins the sentence and that the keyword is
not named.

Assisted-by: Claude

* parity-tests: pin isinstance over a class that overrides __class__

`isinstance` reads `obj.__class__` on an MRO miss, so a `@property
__class__` decides the answer and runs on every call: `isinstance(Masked(),
int)` is True here, not the False a miss looks like. Nothing in the CPython
suite runs that shape hot enough to trace.

Counts are printed rather than asserted, so a fold that elided the call,
cached its result, or answered False shows up as a diff against CPython.

Scope is recorded in the header and was measured, not assumed: this does not
reach `observed_replay_safe_isinstance`, whose only consumer is the nested
residual abort inside an inline sub-walk. At this call depth the residual is
a plain `call_may_force` and that gate no-ops; substituting the weaker
predicate it used to carry leaves every line byte-identical. The gate's
witness is `bench/synth/foriter_isinstance_class_property_replay.py`.

Assisted-by: Claude

* parity-tests: end isinstance_class_property with the harness's OK line

run.py:172 accepts a case only when the last non-empty stdout line is
"OK"; without it all three runners, cpython included, report the file as
a failure.

Assisted-by: Claude

* intobject: correct the claim that gc_interp is off on the native backends

`w_int_gc_alloc`'s doc justified keeping its `dont_look_inside` boundary on
the grounds that it "costs nothing where the arm is unreachable:
`gc_interp::enabled()` is false on the native backends". `enabled_from_env`
(`gc_interp.rs`) answers true for every `PYRE_GC_INTERP` value except exactly
`0`, so it is on by default on every target, and this arm — not the
`malloc_typed` one `fuse_boxing_alloc` rewrites — is the arm `w_int_new`
takes.

Assisted-by: Claude

* jit: separate the pop fold's lock-guard-free label from its trace-guard order

`w_list_pop_end`'s doc defines "the descended body must hold no guard" as the
absence of a `w_list_lock` acquire/release pair, which is what declines the
fold's sub-walk. `try_walker_orthodox_list_pop` and `list_pop_end_jitcode`
repeated the word without that qualifier, where it reads as a claim about
trace guards.

The sub-walk gets no callee frame, so a guard recorded inside it resumes at
the caller's CALL boundary and re-executes the whole `pop()`. Every guard the
Integer arm can record lands ahead of its `ll_list_int_set_len`: the sole op
after that store is the `w_int_new` call, which `dispatch_inline_call_dir_kind`
short-circuits into `walker_box_int` (`NewWithVtable` + `SetfieldGc`, no guard
recorded) and returns before `run_sub_jitcode_walk`. Nothing asserts the
ordering.

Assisted-by: Claude

* jitcode_dispatch: check the pop fold's guard/store order instead of assuming it

`orthodox_list_pop_commit` descends `w_list_pop_end_inner` with no callee
frame, so a guard recorded inside resumes at the caller's CALL boundary and
re-executes the whole `pop()`. That is sound only while every guard lands
before the body's first committed store. Today it does — the Integer arm's
`ll_list_int_set_len` is followed only by the `w_int_new` call, which
`dispatch_inline_call_dir_kind` short-circuits into `NewWithVtable` +
`SetfieldGc` and records no guard — but nothing read the order.

`subwalk_guard_follows_store` scans the ops recorded since a `TracePosition`
and reports whether a guard follows a `setfield` / `setarrayitem` /
`setinteriorfield`. A `start` past the end of the ops vector reports true: the
window is gone, so an empty read is not an answer.

The commit captures the position before `run_sub_jitcode_walk` and declines
with `OrthodoxSubWalkTraceUnsupported` on a positive read, which cuts the
tentative IR back to the generic residual. The decline takes the same
`w_list_len == len_before` re-read the apply below does: where the arm's store
keeps a runtime binding the sub-walk executed it for real, and cutting back to
a residual that pops again is the double-apply the append side already had to
fix.

Unit test covers guard-then-store, store-then-guard, a store recorded before
the captured position, `SetarrayitemGc`, and a position past the end.

`pyre/bench/synth/list_pop_append.py` still reads 2.2x against its
`max-pypy-ratio=22`; it read 73.5x before the fold existed.

Assisted-by: Claude

* docs: record the spec-vs-implementation ruling the parity review keeps re-deriving

Six review findings across PRs #1001, #1079, #1081, #1085 and #1113 are one
policy question, not six bugs: pyre follows CPython for what a Python program
observes while the review measures every line against PyPy. Nothing in the repo
stated the split, so each cycle re-filed them under sections 1/2.

The ruling is that pyre's implementation is a port of PyPy and pyre's spec is
CPython 3.14. Six of the seven adjudicated cases carry no version delta at all
(`sched_setscheduler` has returned None since 3.3, `PyUnicode_FSConverter` has
accepted bytes since 3.3, PEP 529 surrogatepass is 3.6, `DirEntry` has cached
its `stat_result` since PEP 471), so "3.14" names which CPython to read rather
than a lag PyPy is expected to close.

- AGENTS.md gains the normative section and the six tests in short form.
- The `/parity` skill gains a fourth deviation class, SPEC-DEVIATION, exempt
  from Principle 6's auto-fix (reverting one re-introduces a known bug), plus
  the full procedure with its evidence rules and worked examples.
- `.github/codex-review-prompt.md` replaces the "Python 3.11 vs 3.14" exception
  with the four conditions a section-4 entry must carry.

Structure — names, module paths, control-flow order, data structures, storage
owner, JIT hints — is outside the ruling and follows PyPy unconditionally. A
finding where PyPy's shape serves a mechanism pyre also has stops at PyPy: the
`DirEntry.stat()` object cache is one, since `interp_posix.py:537-542` states
the per-call rebuild is what keeps the allocation virtual.

Assisted-by: Claude

* _sre: drive an ASCII str subject as bytes and read the stored length

`Subject::len()` called `code_points().count()` and `char_to_byte` called
`code_point_indices().nth(pos)`, so every match walked the subject before
the engine started; `Request::new` and `create_cursor` then walked it
again.

Add `Subject::AsciiStr` for a `str` whose code points are one byte each,
selected by `w_str_is_ascii` where `make_ctx` selects `is_ascii()`
(interp_sre.py:246).  It drives the WTF-8 payload as bytes, so a character
position is already a byte offset -- `UnicodeAsciiMatchContext`
(interp_sre.py:52).  `StrDrive` is `count` and cursor arithmetic only and
every unicode decision keys on the compiled pattern's opcode, which is the
property that lets upstream spell that context as a bare `StrMatchContext`
subclass.

`Subject::Str` now carries the object (`ctx.w_unicode_obj`,
interp_sre.py:250) and reads the stored `_len()` and `_index_to_byte`
rather than re-deriving them.  Its positions remain code point indices
that the `Wtf8` driver still resolves by walking; the note on the variant
records what converting the reported spans would take.

`slice_subject`, `empty_subject` and `finish_output` branch on
`is_unicode()`, and `subject_span_bytes` extracts the position mapping and
slices once; `char_len` and `char_slice` are gone.

On an ASCII subject with n=1.6M, `pat.match(s, pos)` measured 130.7us at
pos=0 and 762us at pos=n-10; both are now 0.35us, flat in n and in pos.
A differential run over match/search/fullmatch spans, pos/endpos sweeps,
findall/finditer/split/sub/subn/expand, bytes/bytearray/memoryview, type
mismatches, a str subclass and scanner positions is byte-identical to
CPython 3.14.6 on all 608 lines, as it was before the change.
check.py --backend dynasm: 425/425.

Assisted-by: Claude

* BINARY_SLICE: convert str bounds through the index storage

`binary_slice_values`'s `str` branch collected the byte offset of every
code point in the subject into a `Vec<usize>` to resolve two bounds, so
`s[a:b]` cost the whole string.  A one-character slice of a 200k subject
measured 752us, against 0.42us for the same slice written as a prebuilt
slice object, which reaches `w_str_slice_codepoints` and walks only the
sliced elements.

Read the stored `_length` and convert the two bounds with `_index_to_byte`
(unicodeobject.py:1251), which is what the slice-object path already does.
The clamping and the `.max(s)` on the stop bound are unchanged, and a
bound equal to the count still resolves to the end of the buffer.

`binary_slice_values` is shared with the JIT residual
(`bh_binary_slice_fn`, call_jit.rs:5765), so both consumers get it.

The compiler folds constant bounds to `LOAD_CONST slice` + `BINARY_OP []`
and emits `BINARY_SLICE` only for computed ones, so this is the path
`json/decoder.py` takes with its per-token `s[end:end + 1]`.  Decoding a
flat 208 KB ASCII payload with the pure-Python scanner: 25.85s -> 0.089s,
with the size sweep going from x3.95/x4.13/x9.39 per doubling to
x2.07/x2.62/x1.77.  `s[p:p+1]` on a 200k subject: 752us -> 0.54us.

A differential run over 12 subjects (ASCII, 2/3/4-byte, lone surrogates,
empty, and lengths on the 63/64/65/128 index-storage block boundaries)
against 19x19 bound pairs in both spellings, plus list/tuple/bytes slicing
and slice assignment, is byte-identical to CPython 3.14.6 on all 4430
lines.  check.py --backend dynasm: 425/425.

Assisted-by: Claude

* _sre: resolve non-ASCII str positions through the stored index storage

Subject::Str drove the engine over &Wtf8, whose StrDrive::count counts every
code point and whose create_cursor(n) steps over the first n of them. Both run
once per match, so a scan that restarts at successive positions walked the
subject again on every call.

Add Utf8Drive, which carries the W_UnicodeObject next to the payload and
answers count with w_str_len and create_cursor with w_str_index_to_byte,
minting the cursor at the head of an O(1) suffix reslice. Positions stay code
point indices and stepping delegates to the &Wtf8 impl, so the engine's
position arithmetic is unchanged.

Assisted-by: Claude
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