Skip to content

posix, compile, display: take filesystem names as bytes across the path, compile, repr and environment boundaries - #1027

Merged
youknowone merged 6 commits into
mainfrom
import
Aug 4, 2026
Merged

youknowone merged 6 commits into
mainfrom
import

Conversation

@youknowone

@youknowone youknowone commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

Four independent fixes at the filesystem-name boundary. The first is a
wrong-behaviour bug, not a reporting one.

posix paths aliased onto one another

gateway::fsencode_w ended in String::from_utf8_lossy, and the posix module
used it for every path argument, so a path byte with no UTF-8 spelling folded to
U+FFFD before the syscall was made. Two names differing only in such a byte
became one name:

# a file named '�victim' exists (U+FFFD is valid UTF-8)
os.stat(b"\xffvictim")          # was: succeeds, stats the U+FFFD file
                                # now: FileNotFoundError, as CPython and PyPy
os.path.exists(b"\xffvictim")   # was: True    now: False

Upstream never folds: space.fsencode_w (baseobjspace.py:1970) answers with a
byte string and dispatch_filename (interp_posix.py:56-115) hands those bytes
to the call unchanged. The extraction now yields bytes, and OsStr::from_bytes
carries them to the syscall on unix; the text form stays only where the platform
has no byte spelling of a path.

fsencode_w and fsencode_w_with_kind are deleted rather than left reachable,
and the PyPath / PyPathOrNone macro bindings — which stand for
gateway.py:365 unwrap_spec 'fsencode' — bind Vec<u8>.

OSError.filename now carries the path object the call was given
(interp_posix.py:332 wrap_oserror2(space, e, w_path)), so a bytes path reports
bytes and a str keeps its surrogate escapes. For an os.PathLike, that object is
the result of the single __fspath__ call, not the wrapper that supplied it
(interp_posix.py:211-219 _unwrap_path), so the two travel together in one
record. All eight rows across os.stat / os.open / open / io.FileIO
match CPython and PyPy.

compile() dropped its filename, silently

compiling.py:13 declares unwrap_spec(filename='fsencode', mode='text'), but
builtin_compile bound both as if is_str { ... } else { <default> }:

call CPython PyPy before
compile(s, b"ascii.py", "exec").co_filename 'ascii.py' 'ascii.py' '<string>'
compile(s, b"\xff.py", "exec").co_filename '\udcff.py' '\udcff.py' '<string>'
compile(s, '\udcff.py', "exec") '\udcff.py' '\udcff.py' UnicodeEncodeError
filename with __fspath__ '\udcff.py' '\udcff.py' '<string>'
compile(s, 42, "exec") TypeError TypeError '<string>'
compile(s, bytearray(b"ba.py"), "exec") TypeError 'ba.py' + warning '<string>'
compile(s, "a\x00b.py", "exec") ValueError ValueError accepted
compile(s, "a.py", 42) TypeError TypeError silently exec

The first row needs no exotic encoding: a plain ASCII bytes filename was lost,
and every traceback, warning and frame built from that code object pointed at
<string>. Routing filename through fsencode_bytes_w and mode through
text_w yields the whole table, since the converter already carries the
deprecation warning, both TypeError spellings and the NUL rejection. The
bytearray row is where CPython and PyPy differ; we follow PyPy.

The filename names the whole compilation unit, so nested constants inherit it
when they are boxed — otherwise outer.co_filename was exact while
outer.co_consts[0] and the function's __code__ stayed lossy. That is what
separates it from pycode.py:431, whose constructor and replace filenames
rename only the object being built, so the two get separate entry points.

repr built in a buffer that could not hold what it was given

code_repr formatted source_path, so after c.replace(co_filename=...) it
reported the name the object was compiled under, not the current one. It now
builds from code_filename_bytes as WTF-8, the way pycode.py:570 repr reaches
for the filename's utf8_w.

That made a code object the second thing whose repr can carry a lone surrogate,
and py_repr_wtf8 was a hand-maintained prelude listing such types before
delegating to py_repr, whose String cannot hold one. Frames were on the list
and code objects were not, so repr(code) aborted in w_str_get_value.
Any new such type repeats that, and the delegation also lost the surrogate for
every container — reachable on main today, independent of this change:

class C:
    def __repr__(self): return "s\udcff"
repr(C())     # 's\udcff'   exact
repr([C()])   # was '[s�]'; CPython and PyPy give '[s\udcff]'

So the two are inverted: py_repr_wtf8 / py_str_wtf8 carry the whole
dispatch, py_repr / py_str are lossy views of them for the ~109 callers that
genuinely want a Rust String (error-message interpolation and the like), and
the prelude's per-type carve-outs are deleted rather than extended. dict_repr,
list_repr, tuple_repr, array_repr_wtf8, the deque / set / frozenset /
slice / range / structseq / exception formatting, and the !r conversion in
f-strings, str.format and the descriptor slots build a Wtf8Buf and recurse
through py_repr_wtf8 — the same shape as listobject.py:206-225 _listrepr_inner, which appends space.utf8_len_w(space.repr(w_item)) into a
rutf8.Utf8StringBuilder.

No repr's text changes. ReprGuard::enter and the stack_check() are
untouched, so [[...]], {1: {...}} and the RecursionError on a
200000-deep list all still hold, and every slot keeps the allocation kind it
had (a per-call __repr__ result stays GC-managed rather than becoming an
immortal malloc_raw buffer).

Buffer assembly did cost one thing, caught by the Codex parity review: the
parts.join(", ") it replaced separates by position, while emitting a
separator whenever the buffer is non-empty separates by what was written. An
item whose __repr__ answers "" writes nothing, so the next item lost its
separator — repr([E(), E()]) gave [], and repr(ValueError(E(), E()))
gave ValueError(). All four sites now go by index, dict_repr included
(which was accidentally safe, since its ": " always lands).

putenv wrote its value through a lossy decode

os.putenv / os.unsetenv take both halves through the same 'fsencode'
converter, and posix.environ already hands entries back as bytes, so a value
spelling a byte with no UTF-8 form has to survive the write. They now carry
Vec<u8> to set_var / remove_var — which already accept impl AsRef<OsStr>,
so no change outside pyre was needed — with the NUL rejection and the
interp_posix.py:1762-1769 key rule (= illegal only after index 0, so the
=C: form Windows uses for a working directory stays legal).

Verification

  • pyre/extra_tests/parity_tests (176): no dynasm=FAIL. The one failure is
    type_members_python314.py cpython=FAIL, the system CPython, not pyre.
  • pyre/extra_tests dynasm: 235/241, up from 234; the remaining six are a
    subset of the previous seven.
  • cargo test -p pyre-interpreter --features dynasm: 490 passed, 0 failed.
  • New compile_filename_boundary.py, repr_surrogate_wtf8.py,
    repr_empty_item_separator.py and the extended
    os_boundary_surrogateescape.py pass under CPython and PyPy too; the
    bytearray filename row is left out because CPython and PyPy disagree there.

Not in this change

  • _path_splitroot and the Windows arg_path still go through text. Both are
    text-level operations on a name rather than a syscall boundary, and each
    carries the reason in place.
  • os.execve reports no filename, which is what interp_posix.py:1812,1817
    does and what this branch's base did. CPython reports the program path there;
    the divergence predates this change.
  • A String-returning #[pymethod] is wrapped with w_str_new, which is the
    immortal malloc_raw path — so deque.__repr__ and its neighbours leak per
    call. That predates this change and is a macro-level fix.

Summary by CodeRabbit

  • Bug Fixes
    • Preserved non-UTF-8 filesystem paths and surrogate characters across file operations, process execution, environment handling, and related errors.
    • Improved compile() filename handling, including path-like values, invalid inputs, syntax errors, and code-object representations.
    • Corrected string and representation formatting for nested containers, exceptions, deques, arrays, and empty items.
  • Tests
    • Added comprehensive coverage for path boundaries, surrogate escaping, WTF-8 representations, and filesystem error behavior.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The interpreter preserves filesystem path bytes and original path objects across compile(), code-object metadata, representation formatting, and POSIX operations. Parity tests cover surrogate escapes, invalid paths, syntax errors, frames, filesystem lookup, environment handling, and OSError.filename.

Changes

Filesystem and representation boundaries

Layer / File(s) Summary
Path byte conversion contract
pyre/pyre-interpreter/src/gateway.rs, pyre/pyre-macros/src/lib.rs, pyre/pyre-interpreter/src/host_seam.rs, pyre/pyre-interpreter/src/module/_random/macro_smoke.rs
Path conversion uses filesystem bytes and retains resolved path objects. Typed path bindings now use byte vectors.
Compile filename and code metadata
pyre/pyre-interpreter/src/builtins.rs, pyre/pyre-interpreter/src/pycode.rs, pyre/extra_tests/parity_tests/compile_filename_boundary.py
compile() preserves filename bytes, validates modes, rewrites syntax-error filenames, and propagates filenames to code objects and frames.
WTF-8 representation pipeline
pyre/pyre-interpreter/src/display.rs, pyre/pyre-interpreter/src/_structseq.rs, pyre/pyre-interpreter/src/module/_collections/mod.rs, pyre/pyre-interpreter/src/module/array/mod.rs, pyre/pyre-interpreter/src/runtime_ops.rs, pyre/pyre-interpreter/src/type_methods.rs, pyre/pyre-interpreter/src/typedef.rs, pyre/extra_tests/parity_tests/repr_surrogate_wtf8.py, pyre/extra_tests/parity_tests/repr_empty_item_separator.py
Representation and stringification paths compose WTF-8 values. Tests cover surrogate preservation, empty item representations, recursion, and container formatting.
POSIX byte paths and filename errors
pyre/pyre-interpreter/src/module/posix/interp_posix.rs, pyre/pyre-interpreter/src/builtins.rs, pyre/extra_tests/parity_tests/os_boundary_surrogateescape.py
POSIX operations pass byte paths to host APIs, preserve result kinds, retain path objects in errors, and preserve byte-oriented environment values.

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

Possibly related PRs

  • youknowone/pyre#1001: Both changes preserve filesystem bytes across path handling, code metadata, and parity tests.
  • youknowone/pyre#1018: This change extends related filesystem-byte and WTF-8 filename handling.
  • youknowone/pyre#791: Both changes modify compilation filenames and filesystem encoding in builtins.rs and gateway.rs.

Poem

A rabbit guards each filesystem byte,
While WTF-8 keeps surrogates straight.
Compile records the filename trail,
POSIX errors keep the detail.
Representations hop, intact and bright.

🚥 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 change: byte-preserving filesystem-name handling across POSIX, compile, representation, and environment boundaries.
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 import

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 4, 2026 •

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 239b169).
Updated: 2026-08-04T18:27:56.790Z

Files in the reviewed diff
pyre/extra_tests/parity_tests/compile_filename_boundary.py
pyre/extra_tests/parity_tests/os_boundary_surrogateescape.py
pyre/extra_tests/parity_tests/repr_empty_item_separator.py
pyre/extra_tests/parity_tests/repr_surrogate_wtf8.py
pyre/pyre-interpreter/src/_structseq.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/display.rs
pyre/pyre-interpreter/src/gateway.rs
pyre/pyre-interpreter/src/host_seam.rs
pyre/pyre-interpreter/src/module/_collections/mod.rs
pyre/pyre-interpreter/src/module/_random/macro_smoke.rs
pyre/pyre-interpreter/src/module/array/mod.rs
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
pyre/pyre-interpreter/src/pycode.rs
pyre/pyre-interpreter/src/runtime_ops.rs
pyre/pyre-interpreter/src/type_methods.rs
pyre/pyre-interpreter/src/typedef.rs
pyre/pyre-macros/src/lib.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-interpreter/src/builtins.rs:14918, :14947, :14978, :15022, and :15062 ↔ pypy/module/_io/interp_fileio.py:135-152, :229-247: open() now supplies resolved_path.w_path()—the result of __fspath__()—as OSError.filename. PyPy’s _open_fd and FileIO validation consistently retain the original w_name argument. This regresses PathLike failures: open(PathLike(...)) should expose the wrapper object as .filename, not its resolved str/bytes.

2. Other mismatches introduced by this patch

None.

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

  • pyre/pyre-interpreter/src/builtins.rs:6912-6914 ↔ pypy/module/exceptions/interp_exceptions.py:126-133: direct BaseException.__str__ still calls the lossy base_exception_str() and then w_str_new, whereas PyPy returns space.str(...) without a UTF-8-lossy conversion. Thus BaseException.__str__(BaseException("\udcff")) can replace the lone surrogate; this existed before the patch’s new WTF-8 route for subclass exception __str__.

4. Structural adaptations

  • pyre/pyre-interpreter/src/gateway.rs:1544-1632 ↔ pypy/module/posix/interp_posix.py:140-219: FsEncodedPath uses a Rust GC root scope plus a shadow-stack slot to preserve PyPy Path.w_path alongside filesystem bytes. This is a Rust ownership/GC adaptation; its resolved-path behavior matches PyPy’s Path.
  • pyre/pyre-interpreter/src/display.rs:211-285, :541-548 ↔ pypy/objspace/std/listobject.py:206-225, tupleobject.py:109-116, dictmultiobject.py:375-388: Wtf8Buf substitutes for RPython’s rutf8.Utf8StringBuilder, preserving surrogate-containing repr output that Rust String cannot represent.
  • pyre/pyre-interpreter/src/pycode.rs:1570-1584 ↔ pypy/interpreter/pycode.py:428-432: the CPython-compatible Rust compiler requires a UTF-8 source_path, so pyre keeps non-UTF-8 filename bytes separately and uses a lossy compiler-only fallback. The public co_filename, syntax-error filename, and repr paths use the preserved byte field.

@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: 44bd9b23fd

ℹ️ 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 thread pyre/pyre-interpreter/src/display.rs Outdated
Comment on lines +1028 to +1030
return Ok(pyre_object::w_str_get_wtf8(r)
.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 surrogate-bearing reprs inside containers

When a code object with a byte-only filename is nested in a container, such as repr((compile("", b"\xff.py", "exec"),)), py_repr_wtf8 delegates the tuple to py_repr, which reaches this generic code-object __repr__ dispatch and replaces the filename's \udcff with U+FFFD via to_string_lossy(). Direct repr(code) remains exact because of the new special case, but tuple/list/dict representations diverge from PyPy and no longer contain the actual co_filename; the recursive representation path needs to carry WTF-8 rather than convert nested results to String.

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

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs`:
- Around line 498-503: The `as_bytes` flag is determined from the initial `arg`
before `__fspath__` resolution, but a `PathLike` object may return a different
type after resolution. Replace the separate calls to
`pyre_object::bytesobject::is_bytes_like(arg)` and
`crate::gateway::fsencode_bytes_w(arg)?` with a single call to
`fsencode_bytes_w_with_kind(arg)?` that returns both the bytes and a
discriminator indicating the actual resolved type. Use that discriminator to set
`as_bytes` instead of checking the input directly, ensuring `bytes_mode`
reflects the actual resolved path type.
- Around line 2420-2422: Update the path conversion in the stat/lstat
implementations around fsencode_bytes_w to propagate converter errors directly
with ?, removing the blanket map_err TypeError conversion. Preserve the
converter’s original ValueError, RuntimeError, UnicodeEncodeError, and other
exception behavior while maintaining structural parity between stat and lstat.
🪄 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: 7fc490b0-97be-46a4-bcfb-842c39d45114

📥 Commits

Reviewing files that changed from the base of the PR and between ca1d3d4 and 44bd9b2.

📒 Files selected for processing (10)
  • pyre/extra_tests/parity_tests/compile_filename_boundary.py
  • pyre/extra_tests/parity_tests/os_boundary_surrogateescape.py
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/display.rs
  • pyre/pyre-interpreter/src/gateway.rs
  • pyre/pyre-interpreter/src/host_seam.rs
  • pyre/pyre-interpreter/src/module/_random/macro_smoke.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-interpreter/src/pycode.rs
  • pyre/pyre-macros/src/lib.rs

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

Copy link
Copy Markdown
Owner Author

Acted on the Codex parity review.

§1 is real and it was mine. OSError.filename retained the argument object,
which is right for a str or bytes path but wrong for an os.PathLike:
interp_posix.py:211-219 _unwrap_path calls __fspath__ and keeps its result
as Path.w_path. Verified rather than assumed —

CPython PyPy pyre before pyre now
os.stat(P('absent')) 'absent' 'absent' P('absent') 'absent'
os.stat(P(b'absent')) b'absent' b'absent' P(b'absent') b'absent'
open(P('absent')), os.listdir(P('absent')) 'absent' 'absent' P('absent') 'absent'

The bytes and the resolved object now travel together in one record, the shape
Path has upstream, so the single __fspath__ call serves both. The record owns
its root scope: a resolved PathLike name is freshly allocated, and unlike the
caller's argument array nothing else keeps it reachable while the call runs.

§3 — three of the four are pyre against both runtimes, so they are fixed here
rather than deferred:

CPython PyPy pyre before
os.execv failure filename None None '/nonexistent'
posix_spawn env key =C: accepted accepted ('=' in key[1:]) ValueError
repr(code) at co_firstlineno == 0 line -1 line -1 line 0

The posix_spawn rule now matches the execve path in the same file rather
than being a third spelling of it.

execve's filename is deliberately left alone. Codex grouped it with
execv, but the two runtimes disagree there — CPython reports the path, PyPy
reports None — so pyre is not wrong against both, and making it "consistent"
with execv would just pick a side.

Also folded in: os.putenv / os.unsetenv take their halves as bytes, which the
PR text had listed as out of scope. It turned out not to need the
rustpython-host_env change I expected — os::set_var is already generic over
AsRef<OsStr>, so the bytes go straight through.

Re-verified at cbcaa14e4c3: parity_tests has no dynasm=FAIL (the lone failure
is type_members_python314.py cpython=FAIL, the system CPython); extra_tests
dynasm 235/241; cargo test -p pyre-interpreter --features dynasm 490 passed, 0
failed. The new assertions — PathLike filenames, the execv and posix_spawn
rules, the -1 line, and the putenv round trip — pass under CPython too.

— commented 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: cbcaa14e4c

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

Some(env_ptrs.as_ptr()),
) as i32;
Err(errno_err(errno, &command))
Err(errno_err_with_filename(errno, args[0]))

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 Use the filename-less wrapper for execve failures

When path is an os.PathLike and the executable is missing, this changed line stores the original wrapper object in OSError.filename, rather than the resolved str/bytes path used by the syscall. More importantly for PyPy parity, pypy/module/posix/interp_posix.py:1810-1817 catches the failed path-based execve with wrap_oserror, which leaves filename unset, just like the newly corrected execv path above; use the filename-less error conversion here as well.

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

Useful? React with 👍 / 👎.

…ailure names

`gateway::fsencode_w` ended in `String::from_utf8_lossy`, and the posix module
used it for every path argument, so a path byte with no UTF-8 spelling folded
to U+FFFD before the syscall was made and distinct names aliased onto one
another: `os.stat(b"\xffvictim")` answered for the file named `"�victim"`,
and `os.path.exists` on it returned True. Take the bytes through to the call
instead, converting to a `Path` with `OsStr::from_bytes` on unix and keeping
the text form only where the platform has no byte spelling.

Delete `fsencode_w` and `fsencode_w_with_kind` rather than leaving the lossy
converters reachable; the `PyPath` / `PyPathOrNone` macro bindings, which stand
for `gateway.py:365` `unwrap_spec` `'fsencode'`, now bind `Vec<u8>`.

`OSError.filename` carries the argument object the call was given, matching
`interp_posix.py:332 wrap_oserror2(space, e, w_path)`, so a bytes path reports
bytes and a str keeps its surrogate escapes instead of both being respelled
through a lossy decode.

Assisted-by: Claude
… one

`code_repr` formatted `source_path`, the compiler-side spelling, so after
`c.replace(co_filename=...)` it reported the name the object was compiled
under rather than the current `co_filename`. Build it from
`code_filename_bytes` as WTF-8, the way `pycode.py:570 repr` reaches for the
filename's `utf8_w`, and give the code type the same exact `py_repr_wtf8`
branch the frame type has.

That makes a code object the second thing whose repr can carry a lone
surrogate, which the plain-`String` readers abort on: `repr([obj])` for any
`__repr__` returning one already panicked in `w_str_get_value` where CPython
returns the string. Those readers now drop the surrogate the way every other
`String` consumer in this file does; `py_repr_wtf8` remains the accessor that
keeps the result exact.

Assisted-by: Claude
`compiling.py:13` declares `unwrap_spec(filename='fsencode', mode='text')`,
but `builtin_compile` bound both with `if is_str { ... } else { <default> }`,
so anything else was swallowed: `compile(src, b"ascii.py", "exec")` recorded
`"<string>"` — no surrogate needed to lose the name — and bytes, `__fspath__`,
`int` and `bytearray` did the same, a surrogate str raised UnicodeEncodeError,
an embedded NUL was accepted, and a non-str mode became "exec". Route them
through `fsencode_bytes_w` and `text_w`, which already carry the deprecation
warning, the two TypeError spellings and the NUL rejection.

The filename names the whole compilation unit, so the nested constants the
code object still holds unrealized inherit it when they are boxed; that is
what separates it from `pycode.py:431`, whose constructor and `replace`
filenames rename only the object being built. A SyntaxError out of the same
call reports the filename the successful compile would have recorded.

Assisted-by: Claude
…, and three parity fixes

`OSError.filename` retained the argument object, which is right for a `str` or
`bytes` path but not for an `os.PathLike`: `interp_posix.py:211-219 _unwrap_path`
calls `__fspath__` and keeps its *result* as `Path.w_path`, so
`os.stat(P("absent"))` names `"absent"`, not `P`. Carry the encoded bytes and the
resolved object together, as that record does, so the single `__fspath__` call
serves both.

The record owns its root scope, since a resolved `PathLike` name is freshly
allocated and — unlike the caller's argument array — nothing else keeps it
reachable while the call runs.

Alongside, three divergences from both runtimes:

- `execv` attached a filename where `interp_posix.py:1814-1817` wraps with none.
  `execve` is left as it is: there CPython reports the path and PyPy reports
  None, so pyre is not in the wrong against both.
- `posix_spawn` rejected every `=` in an environment key, while
  `interp_posix.py:1762-1769` rejects it only after index zero, which is what
  the `execve` path in the same file already implements.
- `pycode.py:570-572` reports a zero `co_firstlineno` as line -1.

`os.putenv` / `os.unsetenv` take their two halves as bytes for the same reason
paths do: `posix.environ` already hands entries back as bytes, so a value
spelling a byte with no UTF-8 form must survive the write.

Assisted-by: Claude
`py_repr_wtf8` / `py_str_wtf8` carried a prelude for the few types known to
carry a lone surrogate and delegated everything else to `py_repr` / `py_str`,
whose `String` cannot hold one.  Move the whole dispatch into the WTF-8
functions and make `py_repr` / `py_str` lossy views of them, which drops the
prelude's per-type carve-outs for frames and code objects.

`dict_repr`, `list_repr`, `tuple_repr`, `array_repr_wtf8`, the deque, set,
frozenset, slice, range, structseq and exception formatting, and the `!r`
conversion in f-strings, `str.format` and the descriptor slots now build a
`Wtf8Buf` and recurse through `py_repr_wtf8`, so `repr([C()])` for a
`__repr__` returning `"s\udcff"` answers `[s\udcff]` rather than `[s�]`.
`ReprGuard::enter` and the `stack_check()` are unchanged, and each slot keeps
the allocation kind it had.

Adds pyre/extra_tests/parity_tests/repr_surrogate_wtf8.py.

Assisted-by: Claude
@youknowone youknowone changed the title posix, compile: take filesystem names as bytes across the path, compile and repr boundaries posix, compile, display: take filesystem names as bytes across the path, compile, repr and environment boundaries Aug 4, 2026

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/youknowone/pyre/blob/a67823489e4a33d599e390b5c2698252df131720/pyre-interpreter/src/display.rs#L243-L244
P2 Badge Track item count when inserting repr separators

When an element's valid __repr__ returns "", appending it leaves out.len() at 1, so the next element is treated as the first: repr([E(), 1]) renders as [1] rather than [, 1]. tuple_repr has the identical condition, and multi-argument exception formatting similarly checks an accumulated buffer; use explicit item/first state rather than rendered buffer length to preserve separators.

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

…ck out of execve's error

Building a container's repr in a buffer replaced the `parts.join(", ")` the
list, tuple and exception paths used with a separator emitted when the buffer
was already non-empty.  An item whose `__repr__` answers `""` writes nothing,
so the next item lost its separator: `repr([E(), E()])` gave `[]` rather than
`[, ]`, and `repr(ValueError(E(), E()))` gave `ValueError()`.  Emit by position
instead, the way `listobject.py:213-223`, `tupleobject.py:114`,
`dictmultiobject.py:388` and `interp_exceptions.py:135-147` join theirs.
`list_repr` also stops rather than skips when an item is gone, per
`listobject.py:217-221`.

`os.execve` reported the program path as `OSError.filename`;
`interp_posix.py:1812,1817` wraps the failure with `wrap_oserror`, which names
no file, as `execv` already did.

Documents `py_repr_wtf8` against `listobject.py:206-225`, whose
`rutf8.Utf8StringBuilder` the `Wtf8Buf` stands in for.

Adds pyre/extra_tests/parity_tests/repr_empty_item_separator.py.

Assisted-by: Claude

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

🤖 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_boundary_surrogateescape.py`:
- Around line 183-188: Update the subprocess validation around read_back so a
nonzero returncode fails the test and reports the child’s captured output; only
perform the existing stdout equality assertion after confirming the child
completed successfully.

In `@pyre/pyre-interpreter/src/_structseq.rs`:
- Line 175: Replace each transient representation or formatting call to
w_str_from_wtf8 with w_str_from_wtf8_managed in
pyre/pyre-interpreter/src/_structseq.rs:175,
pyre/pyre-interpreter/src/module/_collections/mod.rs:1305,
pyre/pyre-interpreter/src/module/array/mod.rs:959,
pyre/pyre-interpreter/src/type_methods.rs:1676, and both dictionary-view and
union representation sites in pyre/pyre-interpreter/src/typedef.rs:6728-6730 and
9114-9116.

In `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs`:
- Around line 2447-2449: Update stat_impl’s fsencode_path_w call to propagate
the converter result directly with ?, removing the blanket map_err that rewrites
errors as TypeError; preserve the converter’s ValueError for embedded NULs and
unchanged exceptions from user __fspath__. Match the existing chown_entry
handling pattern.
🪄 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: 4c1f80ea-e483-4407-abcf-2c5b3e9b5c01

📥 Commits

Reviewing files that changed from the base of the PR and between 44bd9b2 and 239b169.

📒 Files selected for processing (18)
  • pyre/extra_tests/parity_tests/compile_filename_boundary.py
  • pyre/extra_tests/parity_tests/os_boundary_surrogateescape.py
  • pyre/extra_tests/parity_tests/repr_empty_item_separator.py
  • pyre/extra_tests/parity_tests/repr_surrogate_wtf8.py
  • pyre/pyre-interpreter/src/_structseq.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/display.rs
  • pyre/pyre-interpreter/src/gateway.rs
  • pyre/pyre-interpreter/src/host_seam.rs
  • pyre/pyre-interpreter/src/module/_collections/mod.rs
  • pyre/pyre-interpreter/src/module/_random/macro_smoke.rs
  • pyre/pyre-interpreter/src/module/array/mod.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-interpreter/src/pycode.rs
  • pyre/pyre-interpreter/src/runtime_ops.rs
  • pyre/pyre-interpreter/src/type_methods.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-macros/src/lib.rs

Comment on lines +183 to +188
read_back = subprocess.run(
[sys.executable, "-c", "import os,sys; sys.stdout.buffer.write(os.environb[b'%s'])" % NAME],
capture_output=True,
)
if read_back.returncode == 0:
assert read_back.stdout == b"v\xff", read_back.stdout

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Report the child failure instead of skipping the assertion.

The assertion runs only when returncode == 0. If the child process fails for any reason, the test passes without checking the environment value, so a regression in putenv byte handling stays hidden. Surface the child output when the run fails.

♻️ Proposed change
     read_back = subprocess.run(
         [sys.executable, "-c", "import os,sys; sys.stdout.buffer.write(os.environb[b'%s'])" % NAME],
         capture_output=True,
+        check=False,
     )
-    if read_back.returncode == 0:
-        assert read_back.stdout == b"v\xff", read_back.stdout
+    assert read_back.returncode == 0, read_back.stderr
+    assert read_back.stdout == b"v\xff", read_back.stdout
📝 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
read_back = subprocess.run(
[sys.executable, "-c", "import os,sys; sys.stdout.buffer.write(os.environb[b'%s'])" % NAME],
capture_output=True,
)
if read_back.returncode == 0:
assert read_back.stdout == b"v\xff", read_back.stdout
read_back = subprocess.run(
[sys.executable, "-c", "import os,sys; sys.stdout.buffer.write(os.environb[b'%s'])" % NAME],
capture_output=True,
check=False,
)
assert read_back.returncode == 0, read_back.stderr
assert read_back.stdout == b"v\xff", read_back.stdout
🧰 Tools
🪛 Ruff (0.16.1)

[error] 183-183: subprocess call: check for execution of untrusted input

(S603)


[warning] 183-183: subprocess.run without explicit check argument

Add explicit check=False

(PLW1510)


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

Replace with format specifiers

(UP031)

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

In `@pyre/extra_tests/parity_tests/os_boundary_surrogateescape.py` around lines
183 - 188, Update the subprocess validation around read_back so a nonzero
returncode fails the test and reports the child’s captured output; only perform
the existing stdout equality assertion after confirming the child completed
successfully.

parts.join(", ")
)))
out.push_str(")");
Ok(pyre_object::w_str_from_wtf8(out))

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

Use the managed constructor for transient representation strings.

w_str_from_wtf8 allocates the Unicode object outside the managed GC path. These runtime paths create a new temporary string on each representation or formatting call. Repeated calls can retain allocations and increase process memory.

  • pyre/pyre-interpreter/src/_structseq.rs#L175-L175: replace w_str_from_wtf8 with w_str_from_wtf8_managed.
  • pyre/pyre-interpreter/src/module/_collections/mod.rs#L1305-L1305: use the managed constructor for the deque representation.
  • pyre/pyre-interpreter/src/module/array/mod.rs#L959-L959: use the managed constructor for the array representation.
  • pyre/pyre-interpreter/src/type_methods.rs#L1676-L1676: use the managed constructor for the temporary !r conversion result.
  • pyre/pyre-interpreter/src/typedef.rs#L6728-L6730: use the managed constructor for dictionary-view representations.
  • pyre/pyre-interpreter/src/typedef.rs#L9114-L9116: use the managed constructor for union representations.
📍 Affects 5 files
  • pyre/pyre-interpreter/src/_structseq.rs#L175-L175 (this comment)
  • pyre/pyre-interpreter/src/module/_collections/mod.rs#L1305-L1305
  • pyre/pyre-interpreter/src/module/array/mod.rs#L959-L959
  • pyre/pyre-interpreter/src/type_methods.rs#L1676-L1676
  • pyre/pyre-interpreter/src/typedef.rs#L6728-L6730
  • pyre/pyre-interpreter/src/typedef.rs#L9114-L9116
🤖 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/_structseq.rs` at line 175, Replace each transient
representation or formatting call to w_str_from_wtf8 with
w_str_from_wtf8_managed in pyre/pyre-interpreter/src/_structseq.rs:175,
pyre/pyre-interpreter/src/module/_collections/mod.rs:1305,
pyre/pyre-interpreter/src/module/array/mod.rs:959,
pyre/pyre-interpreter/src/type_methods.rs:1676, and both dictionary-view and
union representation sites in pyre/pyre-interpreter/src/typedef.rs:6728-6730 and
9114-9116.

Comment on lines +2447 to 2449
let path = crate::gateway::fsencode_path_w(args[0]).map_err(|_| {
crate::PyError::type_error("stat: path should be string, bytes, os.PathLike")
})?;

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 | 🟠 Major | ⚡ Quick win

Propagate the path-converter error in stat_impl instead of rewriting it to TypeError.

fsencode_path_w reports an embedded NUL as ValueError, and it lets an exception raised inside a user __fspath__ out unchanged, including RuntimeError and UnicodeEncodeError. The blanket map_err turns all of them into TypeError, so os.stat and os.lstat lose those results. The same file states this rule for chown_entry at lines 4118-4123 and uses ? there.

This repeats a previous review comment that was reported as addressed.

🐛 Proposed fix
-        let path = crate::gateway::fsencode_path_w(args[0]).map_err(|_| {
-            crate::PyError::type_error("stat: path should be string, bytes, os.PathLike")
-        })?;
+        let path = crate::gateway::fsencode_path_w(args[0])?;

As per coding guidelines, when porting RPython/PyPy code, maintain strict line-by-line structural parity; do not take shortcuts.

🤖 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 2447 -
2449, Update stat_impl’s fsencode_path_w call to propagate the converter result
directly with ?, removing the blanket map_err that rewrites errors as TypeError;
preserve the converter’s ValueError for embedded NULs and unchanged exceptions
from user __fspath__. Match the existing chown_entry handling pattern.

Source: Coding guidelines

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