Skip to content

posix: capture readdir d_ino/d_type at scandir enumeration for DirEntry (#68) - #1107

Merged
youknowone merged 9 commits into
mainfrom
str
Aug 8, 2026
Merged

posix: capture readdir d_ino/d_type at scandir enumeration for DirEntry (#68)#1107
youknowone merged 9 commits into
mainfrom
str

Conversation

@youknowone

@youknowone youknowone commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary

Makes os.scandir's DirEntry carry the metadata readdir reports at
enumeration, so inode()/is_dir()/is_file()/is_symlink() answer without a
stat, matching interp_scandir.py / CPython 3.14. Closes the POSIX legs of #68.
Also bundles a #66 doc follow-up, a post-rebase jitstats re-record, a
cherry-picked JIT fix for a base regression the rebase surfaced, and three fixes
for CI reds that were pre-existing base issues unrelated to the scandir work
(see "Bundled CI-red fixes").

Commits

  • §3a inode leg (f052143): native enum_ino: i64 on W_DirEntry; the name
    path captures the readdir d_ino, and inode() returns it without a stat
    (-1 → the existing stat fallback).
  • §3b type leg + fd metadata (78dca77): native enum_type: i32 (the
    d_type byte; default 0 = DT_UNKNOWN). The name path enumerates through
    libc::opendir/readdir so it reads the dirent directly; the descriptor path
    captures the same fields. is_dir/is_file/is_symlink answer from
    enum_type, falling back to a stat only for DT_UNKNOWN or a followed symlink.
  • x86 dynasm: port FRAME_FIXED_SIZE layout + extract shared stack_check_slowpath #66 citation fix (defb136): correct the DirEntry.stat caching doc.
  • jitstats re-record (78ca18c): re-record three drifted synth baselines
    (benign: bridges_compiled −1, guard_failures ↓, new fbw_* = 0).
  • type-name fold fix (8b01d57, cherry-picked -x from jit(wasm): drop the wasm32 arm of the self-recursive root-bridge inline #1106): declines the
    Cls.__name__ fold when its metaclass guard cannot hold — fixes the
    pypy_type_surface base regression the rebase replayed. De-dups on rebase once
    jit(wasm): drop the wasm32 arm of the self-recursive root-bridge inline #1106 lands.

Bundled CI-red fixes

After the type-name fold fix went green locally, PR CI showed two reds; both were
pre-existing base issues, not caused by the scandir work (adjudicated with the
Linux-CI free-control and against the base run at d936eb4be42). Fixing the
pypy_type_surface gate also unmasked a Windows parity red that check.py had
been bailing before (gate failed → parity tests never ran).

  • gcref test flake (d6c2df5, cargo test (ubuntu)): walk_all_gc_tables
    is reachable from any collector test's collection via the globally-registered
    gc_table_extra_root_walker. Under the parallel harness a collection can
    upgrade a Weak a dropping registry test expects dead, transiently
    resurrecting the table ("freed table must not be walked"). Fix: a #[cfg(test)]
    RwLock (walks read, registry tests write), compiled out in production.
    Empirical A/B: pre-fix 37–80 resurrections / 20000 iters → post-fix 0/0; full
    suite 20/20 clean.
  • Windows os.utime pre-epoch (6a7168c, check.py (windows)): the Windows
    path carried timestamps as an epoch-based unsigned Duration, refusing every
    pre-1970 time. Rewritten to CreateFileW+SetFileTime through a FILETIME
    (100-ns ticks from 1601), the write rposix.win32_utime makes; failures go
    through fs_err_with_filename so OSError.winerror is set. Enables the
    Win32_Security feature CreateFileW's signature needs (caught by an isolated
    x86_64-pc-windows-msvc cross-check).
  • pathconf guard (e88f419): os.pathconf_names is POSIX-only (Windows
    CPython lacks it), so guarded that section of os_utime_pathconf_truncate.py
    with sys.platform, matching the file's existing utime/truncate guards.

Verification (macOS, python3.14 oracle)

  • Type/inode predicates byte-identical to python3.14 across file/dir/symlink
    types, cached-after-removal, follow/nofollow, broken symlink, the scandir(fd)
    path, GC stress, and os.scandir("a\0b")ValueError.
  • Real consumers match the oracle: os.walk, glob, pathlib.rglob,
    shutil.copytree/rmtree.
  • pyre/check.pyALL PASSED, 3/3 backends (dynasm 405/405, cranelift
    405/405, wasm 401/401), incl. the unix parity test (oracle OK, pyre OK).
  • Adversarial 4-dimension review of the bundled fixes: gcref/pathconf clean; the
    utime FFI review CONFIRMED and I fixed an io_errfs_err (winerror) bug.
  • Windows compile+behavior of the utime path is verified only by this PR's
    Windows CI leg (a full macOS→Windows cross-compile is blocked by libffi-sys;
    the FFI surface was cross-checked in isolation).

Remaining #68 scope

Windows DirEntry.inode()/is_dir should use the enumeration file index /
FILE_ATTRIBUTE flags instead of a fresh win_stat_fields(). Untestable on
macOS; tracked for Windows CI.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Directory entries can now provide inode and file-type information without additional metadata lookups when available.
    • Windows now supports utime updates for pre-epoch timestamps and directory handles.
    • Added JIT statistics for blackhole adoption and store-journal rollback failures.
  • Bug Fixes

    • Improved scandir behavior and metadata caching across POSIX and Windows.
    • Prevented incorrect specialization of certain __name__ attribute lookups.
  • Tests

    • Updated platform-specific tests to accurately cover supported POSIX and Windows behavior.

binary_int_overflow_local_resume, exc_bridge_entry_guard_not_removed, and
list_append_write_barrier_gc across dynasm/cranelift/wasm: bridges_compiled
drops by one and guard_failures decreases after the trace-time unary int
specialization removes deopts (loops_compiled and loops_aborted unchanged,
internal_compile_panics=0). Adds the fbw_blackhole_adopted_multi_frame,
fbw_blackhole_adopted_single_frame, and fbw_store_journal_rollback_failed
counters (all 0). Values are identical across the three backends.

Assisted-by: Claude
… model

dir_entry_stat caches and returns the built stat_result object
(`entry.stat() is entry.stat()`), which is `posixmodule.c DirEntry_get_stat`,
not `interp_scandir.py descr_stat` (which caches raw stat data and rebuilds a
fresh result per call). The doc comment cited the latter; correct it and note
the object-caching is the 3.14 behavior.

Assisted-by: Claude
…tion

W_DirEntry gains a native enum_ino field.  The name-based scandir path
captures the readdir inode (DirEntryExt::ino(), never a stat) and
dir_entry_inode returns it directly, the way descr_inode returns
self.inode; it is -1 when unavailable (the scandir(fd) path and non-unix
hosts), which falls back to the existing stat.  A primitive field is
GC-safe — the pyre_class macro emits PTR_OFFSETS only for PyObjectRef
fields.

Assisted-by: Claude
Capture the d_type and d_ino readdir reports at scandir enumeration into
W_DirEntry.enum_type / enum_ino. The name path enumerates through
opendir/readdir instead of std::fs::read_dir so it reads the dirent
directly, and the descriptor path captures the same fields through
fd_readdir. is_dir/is_file/is_symlink answer from enum_type unless it is
DT_UNKNOWN or a followed symlink, and inode() answers from enum_ino on the
descriptor path too, both without a stat. A known DT_DIR/DT_REG therefore
answers is_dir()/is_file() from the cached type even after the entry is
removed, as descr_is_dir/is_file do. The wasm, sandbox, and non-host_env
paths keep the std fallback with enum_type DT_UNKNOWN and stat.

Extract readdir_collect and fd_readdir out of fdlistdir, and add
join_dir_name for the enumerated full path.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 7, 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: 41 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: 441822ab-9e6a-40ce-b546-eb9829aec331

📥 Commits

Reviewing files that changed from the base of the PR and between e5eff81 and 0a711dc.

📒 Files selected for processing (4)
  • majit/majit-gc/src/gcreftracer.rs
  • pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
  • pyre/pyre-interpreter/Cargo.toml
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs

Walkthrough

The PR synchronizes GC test registry walks, adds native Windows timestamp handling, preserves POSIX directory metadata in DirEntry, updates JIT benchmark counters, and adds a metaclass check to __name__ specialization.

Changes

Filesystem behavior

Layer / File(s) Summary
Directory enumeration metadata
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
Directory reads preserve inode and d_type metadata. DirEntry queries use cached values and stat fallback.
Windows timestamp updates
pyre/pyre-interpreter/src/module/posix/interp_posix.rs, pyre/pyre-interpreter/Cargo.toml, pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
Windows utime uses CreateFileW and SetFileTime. Windows-specific test guards and the required Windows API feature are updated.

GC registry walk synchronization

Layer / File(s) Summary
Shared GC table walk lock
majit/majit-gc/src/gcreftracer.rs
Tests use a shared read/write lock to coordinate registry walks and table drops. An unlocked traversal helper supports write-locked tests.

JIT statistics baselines

Layer / File(s) Summary
Benchmark counter baselines
pyre/bench/synth/*.jitstats
Cranelift, DynASM, and Wasm statistics add blackhole adoption and store-journal rollback-failure counters.

JIT attribute specialization

Layer / File(s) Summary
Metaclass specialization precondition
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
__name__ folding now requires the receiver class to match the resolved metaclass.

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

Sequence Diagram(s)

sequenceDiagram
  participant PythonCall
  participant POSIXInterpreter
  participant WindowsAPI
  participant FileSystem
  PythonCall->>POSIXInterpreter: call utime(path, times)
  POSIXInterpreter->>WindowsAPI: CreateFileW(path)
  WindowsAPI->>FileSystem: open file or directory handle
  POSIXInterpreter->>WindowsAPI: SetFileTime(FILETIME values)
  WindowsAPI->>FileSystem: update timestamps
  POSIXInterpreter-->>PythonCall: return or report path error
Loading

Possibly related PRs

Poem

A rabbit checks each cached inode bright,

Then locks the GC tables tight.
Windows clocks and JIT counts grow,
While metaclass guards say “no.”
Hop, hop—the runtime’s flow is clear!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: capturing readdir inode and type metadata during scandir enumeration for DirEntry.
✨ 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 str

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

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 0a711dc).
Updated: 2026-08-08T13:03:56.867Z

Files in the reviewed diff
majit/majit-gc/src/gcreftracer.rs
pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
pyre/pyre-interpreter/Cargo.toml
pyre/pyre-interpreter/src/module/posix/interp_posix.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:3864-3867, 2780 ↔ rpython/rlib/rposix_scandir.py:44-52: the patch hard-codes DT_DIR=4, DT_REG=8, and DT_LNK=10, and unconditionally reads dirent.d_type. Upstream obtains these values from target configuration and returns DT_UNKNOWN when the target lacks d_type. On such Unix targets, Pyre either fails to compile or can return an incorrect DirEntry.is_dir/is_file/is_symlink result; main’s stat-based path remained correct.

2. Other mismatches introduced by this patch

None.

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

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:4220-4238, 4334-4424 ↔ pypy/module/posix/interp_scandir.py:88-169: Pyre eagerly reads the whole directory into _entries; close() is a no-op. PyPy owns a live DIR*, advances it in next_w, closes/rewinds it on exhaustion or close(), and warns on finalization. This changes timing of directory errors, visibility of concurrent directory changes, resource lifetime, and close() semantics. The eager-list implementation was already present in upstream/main.

4. Structural adaptations

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:4076-4094 ↔ pypy/module/posix/interp_scandir.py:443-450: Pyre caches and returns a stat_result object, while the local PyPy 3.11 source caches raw stat data and rebuilds the result. This is an intentional Python 3.14 compatibility adaptation.

  • majit/majit-gc/src/gcreftracer.rs:84-95, 164-186 ↔ rpython/jit/backend/llsupport/gcreftracer.py:7-43: the added lock is test-only synchronization around Rust’s parallel test/GC-root-walker environment. Upstream uses a GC-managed GCREFTRACER; Pyre’s Rust-owned tables require the existing external-root architecture, so this has no direct 1:1 RPython representation.

…hold

`try_walker_specialize_load_type_name_attr` takes the metaclass from
`baseobjspace::type_name_obj_fast_path`, which asks `typedef::type`. That falls
back to `gettypefor(ob_type)` when the receiver's `w_class` slot is null
(typedef.rs:234-243; the objects it covers sit in RODATA, so writing the slot
would SIGBUS). The fold then guards the metaclass by reading the raw `w_class`
field, so for such a receiver it emits `guard_value(NULL, type)` — a guard that
fails on every execution and that nothing can discharge, because no later write
fills the slot.

One class reaches it. The `getset_descriptor` type object is built lazily from
inside the init loop (typedef.rs:9501) as a builder for the descriptors other
typedefs install, so it never enters the registry the post-loop sweep walks to
stamp `w_class = type` (typedef.rs:1546-1553). `typedef::type` still answers
`type` through the fallback, so `type(x)` and `x.__class__` are correct and the
null slot is invisible from Python.

`synth/pypy_type_surface` reads `type(type.__dict__[name]).__name__`, so it paid
one guard failure per iteration on every backend: dynasm, cranelift and wasm all
reported `bridges_compiled 5 -> 102, guard_failures 1011 -> 20497` against the
figures #999 `dad2a722907` recorded — 19486 excess failures over 20000
iterations, and 19486/200 = 97 excess bridges, one per `trace_eagerness` bucket.
#1097 `d51ea7f32a0`, which added the fold, merged 83 seconds before #999, and
the recorded figures are the ones this decline reproduces.

Localized by taking the fixture apart: `check_descriptor_kinds` alone carries
all 97 bridges; the receiver's own value is irrelevant (a loop-invariant object
reads the same), the `FOR_ITER` is irrelevant (removing it reads the same), and
`int`, `str`, `list`, `NoneType`, `object`, `type`, `method_descriptor`,
`wrapper_descriptor`, `builtin_function_or_method` and a user class are all
clean. `getattr(C, "__name__")` — same semantics, but the fold declines because
the site's name is not in the code's name table — is clean too.

check.py dynasm 406/406, cranelift 406/406, wasm 402/402, all green.
`synth/pypy_type_surface` reads its recorded 11/5/0/1011 exactly and
`synth/type_name_attr_fold` still reads its own 5/0/1/4, so the fold keeps
applying wherever its guard holds.

The null slot itself is left alone. Stamping `w_class` on the
`getset_descriptor` type object would restore the invariant the sweep's comment
states and let the fold apply there as well, but it moves recorded baselines and
belongs in its own change.

Assisted-by: Claude
(cherry picked from commit 95ec7f2)
walk_all_gc_tables is reachable from any collector test's collection
through the globally-registered gc_table_extra_root_walker once any table
has existed. Under the parallel test harness a collector test's collection
can upgrade a Weak the dropping registry test expects dead, transiently
resurrecting the table and failing "freed table must not be walked".

Add a test-only RwLock: every walk takes the read side, a registry test
takes the write side across its whole drop/observe window. Split the walk
into a locked outer and an unlocked inner so a registry test holding the
write side observes the registry without re-entering the lock. Compiled
out in production, where the STW collector already prevents a concurrent
drop.

Assisted-by: Claude
…nd-trip

The Windows utime path carried its timestamps as an epoch-based unsigned
Duration, so u64::try_from turned away every time before 1970. A FILETIME
counts 100-ns intervals from 1601-01-01, which holds a pre-epoch time as an
ordinary positive count, so convert the seconds/nanoseconds to a FILETIME
and write them with CreateFileW + SetFileTime, the write rposix.win32_utime
makes. Failures go through fs_err_with_filename so the OSError carries
.winerror, as the module's other Windows path calls do. Enable the
Win32_Security feature CreateFileW's signature needs.

Assisted-by: Claude
…f_truncate on Windows

pathconf and pathconf_names are POSIX-only; Windows CPython has neither, so
the section raised AttributeError there under the oracle. Guard it with
sys.platform, matching the file's existing utime/truncate Windows guards,
and refresh the block comment now that SetFileTime writes pre-epoch times.

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

https://github.com/youknowone/pyre/blob/e88f41989e0698d452b00e72ac77085ea2093fbd/pyre-interpreter/src/module/posix/interp_posix.rs#L2738
P1 Badge Gate d_type access on platform support

On Unix targets whose struct dirent has no d_type member, this all(unix) function unconditionally accesses a nonexistent field, so pyre-interpreter no longer builds there. The upstream port explicitly handles this case in rpython/rlib/rposix_scandir.py:49-52 by checking HAVE_D_TYPE and returning DT_UNKNOWN; preserve that feature-gated fallback instead of assuming the Linux/macOS layout and hardcoded constants.

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

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

@youknowone
youknowone merged commit 9614f37 into main Aug 8, 2026
8 checks passed
@youknowone
youknowone deleted the str branch August 8, 2026 12:58
youknowone added a commit that referenced this pull request Aug 8, 2026
`pypy_type_surface` reads bridges_compiled=5 and guard_failures=1011 on all
three backends, the figures #999 `dad2a722907` recorded. #1097 `d51ea7f32a0`
moved them to 102/20497 by folding `Cls.__name__` across a `getset_descriptor`
whose `w_class` slot is null, and #1107 `9614f37fb1e` declined that fold again;
#1107's own message records the fixture reading 11/5/0/1011 afterwards. #1086
`e5eff816849` sits between the two and wrote the regressed pair in as the
baseline, so the restored reading has failed the gate on every runner since.

The same commit reverted three wasm guard_failures counters to their pre-#1106
values while keeping #1106 `4555e3d76ba`'s other fields:
ca_bridge_multiframe_resume_double_call 2592 -> 2581, wasm_ca_trampoline_decline
601 -> 404, recursion_memo_branch 4704 -> 4724. The guest reads #1106's values
both here and on ubuntu-24.04, the only runner that runs wasm, and each
fixture's loops_compiled and bridges_compiled already agree with what both
observe.

Measured with `pyre/check.py --snapshot-diff` per backend and pattern: dynasm
18/18, cranelift 18/18, and wasm 15/15 for each of the three wasm fixtures.

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