Skip to content

majit: open the retrace path, and detect a stale LLBC before it ships wrong field offsets - #1062

Merged
youknowone merged 12 commits into
mainfrom
cel
Aug 5, 2026
Merged

majit: open the retrace path, and detect a stale LLBC before it ships wrong field offsets#1062
youknowone merged 12 commits into
mainfrom
cel

Conversation

@youknowone

@youknowone youknowone commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Twelve commits on top of 6b843fa4b76, in three groups.

1. The retrace path (#33)

closes_on_own_label declined every retrace on every backend, because every jump_to_preamble outcome closes on a foreign token. compile.py:341-394 compile_retrace declines only on InvalidLoop (:368-371), and both jump_to_preamble sites (unroll.py:156, :171) return a full UnrollInfo, so the check had no upstream counterpart.

Removing it exposed EXC_BAD_ACCESS on two of four repros on both native backends — a GuardClass reading the class word of a register holding the unboxed loop counter (ldr x16, [x8], x8 = 4405, INT_TYPE in x16/x17). Root cause: jump_to_preamble retargets the body JUMP at target_tokens[0] and keeps its args (unroll.py:238-242). Upstream's target_tokens[0] is the start label whose args are loop.inputargs, so compile.py:334's assert jump.numargs() == label.numargs() holds by construction. A pyre retrace emits no start label of its own, so target_tokens[0] is the original loop's start label — the portal entry contract, one boxed Ref per frame local — while the optimized loop-carried set is wider and mixed: measured 13 vs 8, 14 vs 9, 13 vs 8.

So the decline moves to jump_to_preamble, where both correct numbers exist together, and raises InvalidLoop — which is unroll.py:242's own escape, landing on compile.py:368-371's cancel. Scoped to retraces: a compile_loop unroll keeps its start label in the same artifact. Counted as MC_DIAG slot 57 retrace_arity_giveup.

All three backends now decide local-vs-external on the token, which is x86/assembler.py:2461-2467 closing_jump's own test. The wasm backend used a trace-global "does this trace have a LABEL" heuristic instead; a census over 362/362 existing synth fixtures reads 0 on a counter of exactly the affected traces, so the change is provably inert for the corpus.

A new synth fixture reaches retrace_neededcut_retrace_from → unroll → jump_to_preamble → the arity give-up. It does not cover a compiled retrace, because no retrace in this tree satisfies the arity contract; before this, zero fixtures reached compile_retrace at all. All three backends record an identical row. The remaining work — unifying pyre's start-label contract with its loop-carried contract — is filed separately.

2. A stale build/llbc shipped wrong field offsets in silence (#44)

preflight_llbc_or_fail tested only that the artefacts exist. The prepass reads them and never the Rust sources (AGENTS.md:48), so a field inserted anywhere but last in a #[repr(C)] struct shifts every following descr offset while the build stays green. codegen_cache_key makes it worse rather than better: it hashes every <crate>/src, so a source edit reliably invalidates the codegen cache and re-runs the prepass — over the same unchanged artefact.

This tree is stale right now. ff42f0e3317 (#950) inserted five abi_*: i64 fields into W_TypeObject before weak_subclasses, terminator and version_tag; the installed artefacts predate it. version_tag is rescued at runtime by offset_of!; the other two are not.

The detector compares each artefact's stamp against the sources on the silent-success exit, using the extractor's own oracle — scripts/llbc_extract.py:815-817 prints the same source_fingerprint() value that :474 writes into the stamp, and :643-652 already skips a crate whose stamp matches. One implementation of the digest, and it is not in build.rs. Warning by default; PYRE_LLBC_STRICT=1 promotes it to cargo::error, PYRE_LLBC_SKIP_FINGERPRINT_CHECK opts out. Every non-answer from the oracle is silence, so an unavailable python3 cannot break the build.

Re-extraction is deliberately not part of this PR.

3. Parity

  • posix.DirEntry / posix.ScandirIterator were instantiable, subclassable and named without their module. interp_scandir.py:468-487 gives the typedef no __new__ and sets acceptable_as_base_class = False; :172-180 likewise for the iterator. descr_reduce_ex (:463-465) is ported too — without it copy.copy(entry) returned a nameless entry whose repr() then raised AttributeError. New fixture is byte-identical to CPython 3.14 on nine probes across both backends. name/path stay writable on purpose: scandir_fn populates them with setattr_str, so a read-only getset would silently ship nameless entries.
  • Three copies of the dead-rd_loop_token comment claimed compile.giveup()'s SwitchToBlackhole is "caught at pyjitpl.py:2906-2907 and falls through to blackhole resume". The raise is at :2923, above the try: at :2926, so except SwitchToBlackhole at :2930-2931 does not catch it and the finally: does not run. Comments only.
  • Two reached_loop_header fallbacks and attach_retrace_to_source_guard's two key identities are now debug_asserts; generate_guards' Constant arm rejects differing constants (virtualstate.py:392-394); the no-collector GcRewriterImpl gets its boehm fields (gc.py:151-162); the walker declines directly where compile_trace could only cancel.

Verification

pyre/check.py green on all three backends at each landing point — final run dynasm 380/380 · cranelift 380/380 · wasm 376/376, CHECKPY_RC=0, HEAD stamps equal at both ends, zero existing .jitstats modified (only the retrace fixture's three new ones added). Parity suite under CPython 3.14: all parity tests pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Python 3.14-compatible behavior for posix.DirEntry and posix.ScandirIterator, including qualified type names and restrictions on instantiation, subclassing, copying, and pickling.
    • Added a retracing benchmark covering accumulator type changes.
  • Bug Fixes

    • Improved JIT handling for loop targets, retracing, garbage collection configurations, and control-flow edge cases.
    • Corrected loop and type matching behavior to prevent invalid compilation paths.
  • Diagnostics

    • Expanded JIT diagnostics and statistics for retracing, loop compilation, and rejected optimization cases.

…tant arm

virtualstate.py:396-399 settles a LEVEL_CONSTANT incoming statically and
raises VirtualStatesCantMatch("different constants") before the runtime-box
test at :400-405. pyre had only the equal-constants arm, so two differing
constants fell through to the runtime test and emitted a GUARD_VALUE whenever
the concrete box happened to equal the target constant.

Add the `(Constant(_), Constant(_)) => Err(())` arm, and document that the
`info_type_matches` gate at :1470-1476 is what rejects a Virtual/VArray/
VStruct/VArrayStruct incoming against a NotVirtual expected (the port of
virtualstate.py:392-394 and :525-529), so the Constant arm is never entered
with a virtual incoming.

Two regression tests: differing constants with a runtime box equal to the
target constant, and a Constant target against a VArray incoming.

Assisted-by: Claude
…reachable

The CloseLoop arm's `self.sym.as_ref()` match and the bridge-close
`resolved_key.unwrap_or(bridge_key)` both carry a release fallback where
upstream has none: `reached_loop_header` (pyjitpl.py:2974-2989) runs on a live
MIFrame, and the cell token a close resolves is `jump_op.getdescr()`
(unroll.py:196, :321-322), an object identity that cannot miss.

Add a debug_assert at each site recording why the fallback is unreachable —
`merge_point` returns early on `sym.is_none()` and `trace_fn` never receives
`&mut self`; `current_trace_green_key()` always answers while `is_tracing()`.
Release behaviour is unchanged.

Assisted-by: Claude
…AG 54-56

Three sites on the merge-point path filter or fall back where upstream
asserts or acts unconditionally. Each is now tallied where it FIRES:

- 54 `has_merge_point_with_shape_assert` discards a same-green-key merge point
  whose `green_boxes` length differs from `live_args_len`; pyjitpl.py:3020
  `assert len(original_boxes) == len(live_arg_boxes)` asserts instead. The
  `.any()` becomes an explicit reverse loop with the same short-circuit.
- 55 `register_retrace_merge_point` returns without registering when a jump arg
  carries no intrinsic type; pyjitpl.py:3059-3060 appends unconditionally. The
  early return also suppresses the `keep_tracing_after_close` write, so the
  trace is abandoned without appearing in `loops_aborted`.
- 56 `close_header_pc` falls back to `self.header_pc` when the walk recorded no
  close greens; pyjitpl.py:3021 `same_greenkey` always compares the actual
  closing boxes. The `map_or` becomes a `match`.

Add `MC_DIAG_SLOTS` so `MC_DIAG` and `MC_DIAG_LABELS` cannot drift in length,
and mirror the three labels in pyre-wasm-runner's hand-copied array. 55 and 56
are only reachable under a non-zero `retrace_limit` (default 0), so a
default-parameter run reading 0 on them proves nothing.

Assisted-by: Claude
…ntities

compile.py:797-811 `ResumeGuardDescr.compile_and_attach` installs a finished
retrace under ONE identity — `metainterp.resumekey_original_loop_token` — used
for `new_loop.original_jitcell_token`, `send_bridge_to_backend` and
`record_loop_or_bridge` alike.

pyre binds the artifact to `source_jct` (`set_original_jitcell_token_number`,
`record_loop_or_bridge`) but files `set_next_header_pc`, the `previous_tokens`
read, `caller_recovery_layout`, `build_guard_metadata`'s key and the
`compiled.traces` insert under the caller-supplied `green_key`. Those five name
the right loop only while the two agree.

They agree by construction: the bridge entry derives its key from this token
via `descr_owning_jct(descr)?.green_key()` and seeds both `TraceCtx::green_key`
and `BridgeTraceInfo::green_key` from it. Two debug_assert_eq! now state it,
as `compile_bridge` already does for its own origin key. The retrace-budget
charges (`record_target_token`, `set_retraced_count`) follow the loop being
entered (unroll.py:213-215, :290-297) and are deliberately not covered.

Assisted-by: Claude
…boehm fields

gc.py:653-664 `get_ll_description(gcdescr)` selects `GcLLDescr_boehm` when
`gcdescr is None`, so upstream has no "no collector" state — that configuration
IS boehm, and gc.py:151-162 is its field block. Both backends already took the
base-class answer for two of the four collector-sourced values
(`can_use_nursery_malloc -> False` spelled `max_nursery_size: 0`,
`write_barrier_descr = None`); the other two were hardcoded to their
incminimark values.

Retain the collector probe's `Option` to derive `is_boehm`, then:
- `malloc_zero_filled` takes gc.py:153 `True` with no collector, matching what
  the raw malloc fallbacks (built on `libc::calloc`) actually do. Both settings
  are inert here (rewrite.py:499-500, :521-522).
- `fielddescr_tid` takes gc.py:157 `None` with no collector, so
  gen_initialize_tid (rewrite.py:914-918) emits nothing — the raw malloc
  fallbacks stamp the header themselves.

Assisted-by: Claude
The walker's interp-origin close falls back to `compile_trace(key, &live_args,
None)` when `compile_trace_entry_data` answers `None`. That call cannot
compile: `compile_trace`'s `None`-origin arm demands the entry-bridge payload
(compile.py:1002-1021 `ResumeFromInterpDescr` needs an original green key to
attach to) and returns `Cancelled` for every input — after deep-cloning the
trace-so-far and lowering its snapshot pool.

Answer `Cancelled` at the site so the give-up reads as one. Behaviour is
unchanged. `compile_trace_entry_data` reads `trace_ctx().root_green_key()` and
`trace_meta()`, both installed by every `JitDriver` trace start, so the arm is
unreachable from a jd0 walk; only `MetaInterp::force_start_tracing` opens a
tracer with no session envelope.

Assisted-by: Claude
… last LABEL

find_loop_label_index answered with the trace's trailing LABEL whenever the
trace had one. x86/assembler.py:2461-2467 closing_jump instead tests
`target_token in self.target_tokens_currently_compiling`: a JUMP naming a token
this compilation does not define takes the :2467 `else` arm and jumps
absolutely into another trace, even when this trace defines labels of its own.
Follow the descr when the closing JUMP carries one; keep the last-LABEL answer
when it does not.

is_resumable_peeled and resumable_label_count delegate to the same function, so
key_dispatch, num_labels and has_preamble follow.

Guard the key-0 publication on the first LABEL sitting at op 0. Key 0 enters
the function at its first op, so publishing a LABEL that has work before it
would let a chaining bridge re-run that work.

Assisted-by: Claude
… arity give-up in jump_to_preamble

compile_retrace declined every trace whose closing JUMP did not name a label in
that trace, which is every jump_to_preamble outcome and so every retrace on
every backend. compile.py:341-394 compile_retrace declines only on InvalidLoop
(:368-371), and both jump_to_preamble sites (unroll.py:156 and :171) return a
full UnrollInfo. Delete the check; all three backends now decide
local-vs-external on the token, as x86/assembler.py:2461-2467 does.

Removing it exposed EXC_BAD_ACCESS on two of four repros on both native
backends: a GuardClass read the class word of a register holding the unboxed
loop counter. jump_to_preamble retargets the body JUMP at target_tokens[0] and
keeps its args (unroll.py:238-242). Upstream's target_tokens[0] is the start
label whose args are loop.inputargs, so compile.py:334's
`assert jump.numargs() == label.numargs()` holds by construction. A pyre
retrace emits no start label of its own, so target_tokens[0] is the original
loop's start label -- the portal entry contract, one boxed Ref per frame local
-- while the optimized loop-carried set is wider and mixed: measured 13 vs 8,
14 vs 9, 13 vs 8.

Give up in jump_to_preamble when emit_start_label is clear and the two arities
differ, returning InvalidLoop, which is unroll.py:242's own escape and lands on
compile.py:368-371's cancel. A compile_loop unroll keeps its start label in the
same artifact, so its preamble target is local and the mismatch cannot arise.
Count the give-up in MC_DIAG slot 57 retrace_arity_giveup.

The comment above preamble_target claimed force_box_for_end_of_preamble
(unroll.py:126-127) leaves the body JUMP's args carrying the types the preamble
Label declares. That is a per-arg property and says nothing about the arg list;
restate it.

Assisted-by: Claude
A loop-carried accumulator flips int -> float partway through, so the guard at
that position fails repeatedly and the bridge grown there closes with a virtual
state matching no existing target. The optimizer then asks for a retrace
(compile.py:1085 retrace_needed). retrace_limit defaults to 0
(rpython/rlib/jit.py:595), so the fixture raises it to 5 through
pypyjit.set_param, which is the only route that reaches the wasm guest.

Coverage is retrace_needed -> cut_retrace_from -> the unroll pass ->
jump_to_preamble -> the arity give-up; the retrace is not assembled. No other
fixture in the corpus reaches compile_retrace at all.

All three backends record the same row (loops_aborted=5, loops_compiled=1,
bridges_compiled=1, guard_failures=1202). No max-pypy-ratio line.

Assisted-by: Claude
…rces it models

preflight_llbc_or_fail tested only that build/llbc/{pyre-object,pyre-interpreter,
pyre-jit}.ullbc exist. The prepass reads those artefacts and never the Rust
sources (AGENTS.md:48), so a field inserted anywhere but last in a #[repr(C)]
struct shifts every following descr offset while the build stays green.
codegen_cache_key hashes every <crate>/src, so a source edit does invalidate the
codegen cache and re-run the prepass — over the same unchanged artefact.

Compare each artefact's stamp against the sources on the silent-success exit.
The oracle is the extractor's own: scripts/llbc_extract.py:815-817 prints the
same source_fingerprint() value that :474 writes into the stamp's `source=`
line, and :643-652 already skips a crate whose stamp still matches, so the
digest has one implementation and it is not in build.rs. `features=` and
`layout_targets=` are replayed from the stamp into CARGO_FEATURES and
LLBC_LAYOUT_TARGETS, because fingerprint_inputs (:255-360) walks the dependency
closure under the feature set and the cross-target layout set in force at
extraction time.

Warning by default; PYRE_LLBC_STRICT=1 promotes the same finding to
cargo::error, PYRE_LLBC_SKIP_FINGERPRINT_CHECK opts out. Every non-answer from
the oracle — spawn failure, non-zero exit, unparseable stdout, a 120s deadline —
is silence, so an unavailable python does not break the build. main() already
returns before preflight_llbc_or_fail when MAJIT_LLBC_EXTRACTION=1, so the check
cannot fire while the artefact it inspects is being produced.

All three stamps in this tree are stale: W_TypeObject gained five abi_* i64
fields in ff42f0e, positioned before weak_subclasses, terminator and
version_tag, after the installed artefacts were extracted.

Assisted-by: Claude
Both types were instantiable, subclassable, and named without their module.
interp_scandir.py:468-487 gives W_DirEntry.typedef no __new__ and sets
acceptable_as_base_class = False at :487; :172-180 does the same for
W_ScandirIterator. typedef.py:55 `acceptable_as_base_class = '__new__' in
rawdict` is the rule, and typedef.py:754 asserts it for PyFrame, whose port at
typedef.rs:534-539 is the shape followed here.

Name the typedefs posix.DirEntry (interp_scandir.py:469) and
posix.ScandirIterator (:173), so __module__ reports posix and every
type-name-bearing message is spelled the way the typedef spells it.

Port descr_reduce_ex (interp_scandir.py:463-465). The flag-driven refusal in
reduce_protocol_app.py:13-14 already fires, but spells the type with the bare
__name__ where %T is error.py:592-593 `space.type(value).name`, the qualified
name. Without it copy.copy(entry) returned a nameless entry whose repr then
raised AttributeError.

scandir_fn allocates through pyre_object::w_instance_new, which never enters
type.__call__, so the producer is unaffected. name and path stay writable:
scandir_fn populates them with setattr_str and discards the result, so a
read-only getset would silently ship nameless entries — that needs a storage
migration first.

The new fixture is byte-identical to CPython 3.14 on nine probes across both
backends.

Assisted-by: Claude
…l flow upstream does not have

Three copies of the comment on the dead `rd_loop_token` weakref said
`compile.giveup()`'s `SwitchToBlackhole(ABORT_BRIDGE)` is "caught at
pyjitpl.py:2906-2907, falling through to blackhole resume". The raise is at
pyjitpl.py:2923 and sits above the `try:` at :2926, so the `except
SwitchToBlackhole` at :2930-2931 does not catch it and the `finally:` at
:2932-2935 does not run; it leaves handle_guard_failure with nothing traced.

The cited lines were also stale: handle_guard_failure is at pyjitpl.py:2914 and
the weakref read at :2921, not :2890/:2897.

State the phase difference in compile_bridge as well. Upstream's check runs
before create_history (pyjitpl.py:2925); this site is inside the compile, where
upstream asserts instead (compile.py:800). Returning false becomes
CompileOutcome::Cancelled and thence Declined, which keeps the trace alive —
the shape compile.py:1085's `return None` has at a bridge close, since
raise_if_successful does not raise on None.

Comments only.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The pull request updates JIT backend allocation behavior, trace and retrace validation, diagnostic counters, POSIX builtin parity, LLBC freshness checks, and a retrace benchmark.

Changes

Backend runtime configuration

Layer / File(s) Summary
Collector-dependent allocation configuration
majit/majit-backend-cranelift/src/compiler.rs, majit/majit-backend-dynasm/src/runner.rs
Boehm defaults apply when no collector is installed. Active collectors retain their allocation and write-barrier settings.
Bridge failure control-flow documentation
majit/majit-backend-cranelift/src/compiler.rs, majit/majit-backend-dynasm/src/lib.rs
Comments describe blackhole transfer when the owning JIT token is unavailable.

JIT trace control flow and diagnostics

Layer / File(s) Summary
Diagnostic counter contract
majit/majit-metainterp/src/lib.rs, pyre/pyre-wasm-runner/src/main.rs
Diagnostic storage expands to 58 slots with labels for merge-point, retrace, and loop outcomes.
Merge-point and loop validation
majit/majit-metainterp/src/compile.rs, majit/majit-metainterp/src/jitdriver.rs, majit/majit-metainterp/src/optimizeopt/*
Merge-point shape checks, header-PC fallback, loop retargeting, constant matching, and bridge-close assumptions gain validation and diagnostics.
Retrace outcome handling
majit/majit-metainterp/src/pyjitpl.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs, pyre/bench/synth/retrace_accumulator_type_flip.*
Retrace setup records failures, validates keys, cancels missing-origin closes, and adds a type-flip benchmark with statistics.
WASM loop-target publication
majit/majit-backend-wasm/src/codegen.rs, majit/majit-backend-wasm/src/lib.rs
Descriptor-bearing jumps resolve their target labels directly. Non-peeled loops require an entry label at operation index zero.

POSIX builtin parity

Layer / File(s) Summary
POSIX type behavior
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
DirEntry and ScandirIterator use qualified names and reject direct construction and subclassing. DirEntry rejects pickling.
Python 3.14 parity coverage
pyre/extra_tests/parity_tests/dir_entry_uninstantiable_python314.py
Tests cover metadata, scanning, construction, subclassing, serialization, copying, and exact errors.

LLBC artifact freshness

Layer / File(s) Summary
LLBC artifact discovery and freshness checks
pyre/pyre-jit-trace/build.rs
The build script checks required artifacts and compares recorded fingerprints with extractor output. Warnings, errors, and skips depend on environment settings.

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

Possibly related PRs

Poem

A rabbit checks the loops at dawn,
Counts each bridge before it’s gone.
Boehm fields rest in zeroed space,
POSIX types hold their proper place.
Fresh hashes guard the build tonight—
Hop, hop, everything is right!

🚥 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 identifies the retrace-path changes and stale LLBC detection, which are the primary objectives of the pull request.
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 cel

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

❤️ Share

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0a50973a2

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

.arg(crate_name)
.current_dir(repo_root)
.env("CARGO_FEATURES", features)
.env("LLBC_LAYOUT_TARGETS", layout_targets)

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 Honor the extractor's layout-target delimiter

When a stamp was produced with more than one layout sidecar target, scripts/llbc_extract.py records it as space-separated (layout_targets={' '.join(...)}) but the CLI expects LLBC_LAYOUT_TARGETS to be comma-separated. Passing the raw stamp field here makes the fingerprint subprocess see a single invalid target like a b; it exits non-zero, llbc_source_fingerprint returns None, and the stale-LLBC warning/strict error is silently skipped for those cross-target artifacts, leaving frozen offsets undetected. Re-join the stamp targets with commas before setting the env.

AGENTS.md reference: AGENTS.md:L46-L50

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

Inline comments:
In `@majit/majit-backend-wasm/src/lib.rs`:
- Around line 2223-2228: Update the publishability handling around
first_label_at_entry so a rejected non-peeled retrace retracts the previously
published LABEL_TARGETS entry for the shared descriptor before continuing.
Ensure cleanup targets the stale non-peeled retrace entry without removing valid
targets from other loops, and add a regression test covering publication
followed by a failed same-target retrace and subsequent loop-closing JUMP
resolution.

In `@majit/majit-metainterp/src/optimizeopt/unroll.rs`:
- Around line 1844-1881: Update the body_jump_arity calculation near
body_terminal_op to fall back to the actual terminal Jump in body_ops when
body_terminal_op is None, using that Jump’s num_args() value. Keep the existing
body_terminal_op path unchanged so the mismatch check before the retrace
retargeting reflects the same Jump that lines 1904-1905 may retarget.

In `@pyre/extra_tests/parity_tests/dir_entry_uninstantiable_python314.py`:
- Around line 9-11: Extend the parity test for os.DirEntry/posix.DirEntry to
include pickle protocols 0 and 1, asserting TypeError with the same “cannot
pickle 'posix.DirEntry' object” behavior as other protocols. Update the generic
static-type reduction path used by copyreg._reduce_ex as needed so these
protocols produce the matching rejection instead of being excluded.

In `@pyre/pyre-jit-trace/build.rs`:
- Around line 289-306: Update llbc_fingerprint_output to retain access to the
child process until the timeout outcome is known; on recv_timeout expiration,
terminate the child and wait for it to be reaped before returning None. Preserve
the existing successful output parsing and non-success handling, ensuring every
timed-out fingerprint process is cleaned up.
🪄 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: 266ea48c-ab6e-449f-912b-e9955e96f935

📥 Commits

Reviewing files that changed from the base of the PR and between f58c415 and a0a5097.

📒 Files selected for processing (20)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend-dynasm/src/lib.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-wasm/src/codegen.rs
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-metainterp/src/compile.rs
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/lib.rs
  • majit/majit-metainterp/src/optimizeopt/unroll.rs
  • majit/majit-metainterp/src/optimizeopt/virtualstate.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • pyre/bench/synth/retrace_accumulator_type_flip.cranelift.jitstats
  • pyre/bench/synth/retrace_accumulator_type_flip.dynasm.jitstats
  • pyre/bench/synth/retrace_accumulator_type_flip.py
  • pyre/bench/synth/retrace_accumulator_type_flip.wasm.jitstats
  • pyre/extra_tests/parity_tests/dir_entry_uninstantiable_python314.py
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-jit-trace/build.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-wasm-runner/src/main.rs

Comment on lines +2223 to +2228
let first_label_at_entry = ops
.iter()
.position(|op| op.opcode == majit_ir::OpCode::Label)
== Some(0);
let publishable = first_label_at_entry
&& label_descrs.first().is_some_and(|&id| id != 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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline majit/majit-backend-wasm/src/lib.rs --items all --type function \
    | rg -n 'publish_label_target|resolve_cross_loop_jump_target|invalidate_loop' || true
fi

rg -n -C 10 \
  'publish_label_target|resolve_cross_loop_jump_target|LABEL_TARGETS|unpublish|remove.*label.*target' \
  majit --glob '*.rs'

rg -n -C 8 \
  'label_descrs|set_label_block_id|invalidate_loop|jump_to_preamble|retrace|loop-closing' \
  majit pyre --glob '*.{rs,py}'

Repository: youknowone/pyre

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline around relevant symbols =="
ast-grep outline majit/majit-backend-wasm/src/lib.rs --items all --type function \
  | rg -n 'publish_label_target|resolve_cross_loop_jump_target|invalidate_loop|compile_loop|compile_bridge|get_latest' || true

echo "== locate WASM WASM files =="
git ls-files majit/majit-backend-wasm/src | sed -n '1,120p'

echo "== relevant sections in WASM lib =="
sed -n '2160,2265p' majit/majit-backend-wasm/src/lib.rs
printf '\n--- jump resolver section ---\n'
rg -n -C 18 'pub fn resolve_cross_loop_jump_target|resolve_cross_loop_jump_target|LABEL_TARGETS|label.*target|publish_label_target' majit/majit-backend-wasm/src/lib.rs

echo "== backend trait declarations/usages =="
rg -n -C 8 'fn publish_label_target|publish_label_target|fn resolve_cross_loop_jump_target|resolve_cross_loop_jump_target|fn invalidate_loop|invalidate_loop' majit/majit-metainterp/src majit/majit-backend/src majit --glob '*.rs' | sed -n '1,240p'

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== failguard label target implementation =="
rg -n -C 12 'struct LabelTarget|fn publish_label_target|fn label_target|fn unpublish|fn d?e?retr?' majit/majit-backend-wasm/src/failguard.rs

echo "== label_target helper =="
rg -n -C 18 'pub fn label_target|fn label_target|LABEL_TARGETS|label_target' majit/majit-backend-wasm/src/lib.rs majit/majit-backend-wasm/src/failguard.rs

echo "== invalidate_loop implementation =="
rg -n -C 20 'fn invalidate_loop\(&self, token: &JitCellToken\)' majit/majit-metainterp/src majit/majit-backend/src majit/majit-backend-wasm/src --glob '*.rs'

echo "== remove_compiled_loop / target registry cleanup =="
rg -n -C 16 'remove_compiled_loop|compiled_meta|label.*target|LABEL_TARGETS|unpublish|drop.*label' majit/majit-metainterp/src/pyjitpl.rs majit/majit-metainterp/src/jitdriver.rs majit/majit-backend/src/lib.rs majit/majit-backend-wasm/src/failguard.rs --glob '*.rs'

Repository: youknowone/pyre

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Compile WasmLoop layout =="
sed -n '2254,2285p' majit/majit-backend-wasm/src/lib.rs

echo "== WasmLoop drop =="
sed -n '2286,2316p' majit/majit-backend-wasm/src/lib.rs

echo "== CompiledWasmLoop Drop =="
sed -n '614,640p' majit/majit-backend-wasm/src/failguard.rs

echo "== stale-target reachable probe =="
python3 - <<'PY'
from pathlib import Path
import re

lib = Path("majit/majit-backend-wasm/src/lib.rs").read_text()
fail = Path("majit/majit-backend-wasm/src/failguard.rs").read_text()

# Extract the non-peeled publish predicate and any unpublish/remove_label_target calls.
predicate = re.search(
    r'let first_label_at_entry = .*?\n\s*let publishable = .*?&& label_num_args\.first\(\) == Some\(&inputargs\.len\(\)\);',
    lib,
    re.S,
).group(0)

cleanup_names = re.findall(
    r'\b(?:pub\s+)?(?:fn\s+)?([A-Za-z_][A-Za-z0-9_]*(?:<[^>]*>)?\b)\s*\([^)]*\)\s*\{',
    fail + "\n" + lib,
)
cleanup = re.findall(
    r'\b(?:pub\s+)?(?:fn\s+)?(unpublish[_a-z0-9]*|remove_label_target[s]?|retract_label_target[s]?|LABELS?[_a-z0-9]*\s*\[[^\]]*\]\s*=|LABEL_TARGETS.*remove|LABELS?[_a-z0-9]*.*remove|remove.*label.*target[s]?|retrac[ei]ng_siblings[_a-z0-9]*)\b',
    fail + "\n" + lib,
    re.I,
)

print("predicate:")
print(predicate.strip())
print()
print("direct label-target cleanup symbols:")
print("\n".join(sorted(set(c for c in cleanup if re.search(r'label|label.*target|target.*label', c, re.I)))))
print()
print("function names containing possible cleanup words:")
print("\n".join(sorted(set(c for c in cleanup_names if re.search(r'label|label.*target|target.*label|unpublish|retract|dropped|peer|retrace| sibling', c, re.I)))))

# Inspect non-peeled branch immediately for any explicit stale-target cleanup around publishable=false.
m = re.search(
    r'} else \{(?P<else>.*?)let compiled = CompiledWasmLoop \{',
    lib,
    re.S,
).group("else")
print()
print("non-peeled branch first 24 lines:")
for i, line in enumerate(m.splitlines()[:24], 1):
    print(f"{2189+i-1:5}: {line}")
PY

Repository: youknowone/pyre

Length of output: 6052


Retract the stale label target for rejected non-peeled retraces.

LABEL_TARGETS has no unpublish/clean-up path for label targets, and CompiledWasmLoop::drop only removes targets whose func_handle matches the dropped loop. If a publishable non-peeled retrace is followed by the same loop target retrace that fails this branch, the previous entry remains visible to resolve_cross_loop_jump_target; a later loop-closing JUMP can chain to a trace that is not entry-compatible. Add a regression test and retract the previous target when publication is skipped for this shared descriptor.

🤖 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 `@majit/majit-backend-wasm/src/lib.rs` around lines 2223 - 2228, Update the
publishability handling around first_label_at_entry so a rejected non-peeled
retrace retracts the previously published LABEL_TARGETS entry for the shared
descriptor before continuing. Ensure cleanup targets the stale non-peeled
retrace entry without removing valid targets from other loops, and add a
regression test covering publication followed by a failed same-target retrace
and subsequent loop-closing JUMP resolution.

Comment on lines 1844 to +1881
let preamble_arity = exported_renamed_inputargs.len();
let body_jump_arity = body_terminal_op.as_ref().map(|j| j.num_args()).unwrap_or(0);
if crate::majit_log_enabled() {
let body_jump_arity = body_terminal_op.as_ref().map(|j| j.num_args()).unwrap_or(0);
eprintln!(
"[jit] jump_to_preamble: body_jump_args={} preamble_arity={} start_label_args={:?}",
body_jump_arity, preamble_arity, exported_renamed_inputargs,
);
}
// `compile.py:334 assert jump.numargs() == label.numargs()`.
// Upstream can assert because its `target_tokens[0]` is the start
// label whose args ARE `loop.inputargs` — the same loop-carried
// positions the body JUMP carries — so `unroll.py:238-242`'s
// arg-preserving retarget is sound by construction.
//
// A pyre RETRACE has no start label of its own (see
// `emit_start_label`), so `target_tokens[0]` is the ORIGINAL loop's
// start label: the portal's entry contract, one boxed Ref per frame
// local. The optimized loop-carried set is wider and mixed, so the
// retarget can land N values on an M-slot label and the target then
// reads a raw integer as a Ref — measured as EXC_BAD_ACCESS on the
// loop counter inside GuardClass.
//
// Give up, which is `unroll.py:242`'s own escape (it lets
// `send_extra_operation` raise `InvalidLoop`) and lands on
// `compile.py:368-371`'s cancel. Scoped to the retrace: a
// `compile_loop` unroll keeps its start label in the SAME artifact,
// so its preamble target is local and this mismatch cannot arise.
if !self.emit_start_label && body_jump_arity != preamble_arity {
crate::mc_diag_bump(57);
if crate::majit_log_enabled() {
eprintln!(
"[jit] jump_to_preamble giveup: body JUMP args {body_jump_arity} \
!= preamble LABEL args {preamble_arity}"
);
}
return Err(crate::optimize::InvalidLoop(
"jump_to_preamble: body JUMP arity != preamble LABEL arity",
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Use the actual body Jump when checking arity.

Line 1845 reports zero when body_terminal_op is None. Lines 1904-1905 can still retarget a terminal Jump from body_ops.

A nonzero body Jump can bypass the mismatch check when preamble_arity is zero. A matching body Jump can also be rejected when preamble_arity is nonzero. Derive body_jump_arity from body_ops when body_terminal_op is absent.

Proposed fix
-            let body_jump_arity = body_terminal_op.as_ref().map(|j| j.num_args()).unwrap_or(0);
+            let body_jump_arity = body_terminal_op
+                .as_ref()
+                .map(|jump| jump.num_args())
+                .or_else(|| {
+                    body_ops
+                        .iter()
+                        .rfind(|op| op.opcode == OpCode::Jump)
+                        .map(|jump| jump.num_args())
+                })
+                .unwrap_or(0);

As per coding guidelines, “Port RPython/PyPy code with strict line-by-line structural parity.”

📝 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
let preamble_arity = exported_renamed_inputargs.len();
let body_jump_arity = body_terminal_op.as_ref().map(|j| j.num_args()).unwrap_or(0);
if crate::majit_log_enabled() {
let body_jump_arity = body_terminal_op.as_ref().map(|j| j.num_args()).unwrap_or(0);
eprintln!(
"[jit] jump_to_preamble: body_jump_args={} preamble_arity={} start_label_args={:?}",
body_jump_arity, preamble_arity, exported_renamed_inputargs,
);
}
// `compile.py:334 assert jump.numargs() == label.numargs()`.
// Upstream can assert because its `target_tokens[0]` is the start
// label whose args ARE `loop.inputargs` — the same loop-carried
// positions the body JUMP carries — so `unroll.py:238-242`'s
// arg-preserving retarget is sound by construction.
//
// A pyre RETRACE has no start label of its own (see
// `emit_start_label`), so `target_tokens[0]` is the ORIGINAL loop's
// start label: the portal's entry contract, one boxed Ref per frame
// local. The optimized loop-carried set is wider and mixed, so the
// retarget can land N values on an M-slot label and the target then
// reads a raw integer as a Ref — measured as EXC_BAD_ACCESS on the
// loop counter inside GuardClass.
//
// Give up, which is `unroll.py:242`'s own escape (it lets
// `send_extra_operation` raise `InvalidLoop`) and lands on
// `compile.py:368-371`'s cancel. Scoped to the retrace: a
// `compile_loop` unroll keeps its start label in the SAME artifact,
// so its preamble target is local and this mismatch cannot arise.
if !self.emit_start_label && body_jump_arity != preamble_arity {
crate::mc_diag_bump(57);
if crate::majit_log_enabled() {
eprintln!(
"[jit] jump_to_preamble giveup: body JUMP args {body_jump_arity} \
!= preamble LABEL args {preamble_arity}"
);
}
return Err(crate::optimize::InvalidLoop(
"jump_to_preamble: body JUMP arity != preamble LABEL arity",
));
let preamble_arity = exported_renamed_inputargs.len();
let body_jump_arity = body_terminal_op
.as_ref()
.map(|jump| jump.num_args())
.or_else(|| {
body_ops
.iter()
.rfind(|op| op.opcode == OpCode::Jump)
.map(|jump| jump.num_args())
})
.unwrap_or(0);
if crate::majit_log_enabled() {
eprintln!(
"[jit] jump_to_preamble: body_jump_args={} preamble_arity={} start_label_args={:?}",
body_jump_arity, preamble_arity, exported_renamed_inputargs,
);
}
// `compile.py:334 assert jump.numargs() == label.numargs()`.
// Upstream can assert because its `target_tokens[0]` is the start
// label whose args ARE `loop.inputargs` — the same loop-carried
// positions the body JUMP carries — so `unroll.py:238-242`'s
// arg-preserving retarget is sound by construction.
//
// A pyre RETRACE has no start label of its own (see
// `emit_start_label`), so `target_tokens[0]` is the ORIGINAL loop's
// start label: the portal's entry contract, one boxed Ref per frame
// local. The optimized loop-carried set is wider and mixed, so the
// retarget can land N values on an M-slot label and the target then
// reads a raw integer as a Ref — measured as EXC_BAD_ACCESS on the
// loop counter inside GuardClass.
//
// Give up, which is `unroll.py:242`'s own escape (it lets
// `send_extra_operation` raise `InvalidLoop`) and lands on
// `compile.py:368-371`'s cancel. Scoped to the retrace: a
// `compile_loop` unroll keeps its start label in the SAME artifact,
// so its preamble target is local and this mismatch cannot arise.
if !self.emit_start_label && body_jump_arity != preamble_arity {
crate::mc_diag_bump(57);
if crate::majit_log_enabled() {
eprintln!(
"[jit] jump_to_preamble giveup: body JUMP args {body_jump_arity} \
!= preamble LABEL args {preamble_arity}"
);
}
return Err(crate::optimize::InvalidLoop(
"jump_to_preamble: body JUMP arity != preamble LABEL arity",
));
🤖 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 `@majit/majit-metainterp/src/optimizeopt/unroll.rs` around lines 1844 - 1881,
Update the body_jump_arity calculation near body_terminal_op to fall back to the
actual terminal Jump in body_ops when body_terminal_op is None, using that
Jump’s num_args() value. Keep the existing body_terminal_op path unchanged so
the mismatch check before the retrace retargeting reflects the same Jump that
lines 1904-1905 may retarget.

Source: Coding guidelines

Comment on lines +9 to +11
Pickle protocols 0 and 1 are deliberately NOT asserted: they never reach
`reduce_newobj`, they land in `copyreg._reduce_ex`, and that leg already
diverges for every static type. It is a separate, pre-existing defect.

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a 'reduce_protocol_app.py' .
rg -n -C 8 '_reduce_ex|reduce_newobj|__reduce_ex__' pyre lib-python
rg -n -C 6 'pickle\.dumps\(entry|range\(2, pickle\.HIGHEST_PROTOCOL' \
  pyre/extra_tests/parity_tests/dir_entry_uninstantiable_python314.py

Repository: youknowone/pyre

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file =="
cat -n pyre/extra_tests/parity_tests/dir_entry_uninstantiable_python314.py

echo "== reduce_protocol_app relevant lines =="
cat -n pyre/pyre-interpreter/src/reduce_protocol_app.py | sed -n '230,360p'

echo "== reduce_protocol.rs relevant lines =="
cat -n pyre/pyre-interpreter/src/reduce_protocol.rs | sed -n '308,380p'
cat -n pyre/pyre-interpreter/src/reduce_protocol.rs | sed -n '1,180p'

echo "== locate DirEntry / dir_entry tests in CPython lib-python =="
rg -n "DirEntry|dir_entry|copyreg|reduce" pyre/pyre-interpreter/extra_tests pyre/extra_tests lib-python -g '*python314*' -g '*.py' | head -200

echo "== object object reduce protocol relevant block =="
cat -n lib-python/3/copyreg.py | sed -n '55,145p'

Repository: youknowone/pyre

Length of output: 34117


🌐 Web query:

CPython 3.14 copyreg._reduce_ex pickle protocol 0 1 object __reduce_ex__ TypeError

💡 Result:

In CPython, including version 3.14, the copyreg._reduce_ex function is a helper used by objects that do not explicitly define their own reduce or reduce_ex methods when using low-level pickle protocols (specifically protocols 0 and 1) [1][2][3]. A TypeError occurring within this context is typically triggered because the object's structure is incompatible with the legacy requirements of these protocols [4][3]. Common causes include: 1. Incompatibility of new: When using protocol 0 or 1, copyreg._reduce_ex attempts to reconstruct the object using a mechanism that relies on class.new [1][3]. If an object is implemented as a C-extension type (or has complex internal state that cannot be initialized by a standard object.new call), this reconstruction can fail with a TypeError, such as object.new(X) is not safe, use X.new [4]. 2. Improper use of slots: Classes that define slots without also providing a getstate method are prone to triggering a TypeError during pickling [1][3]. For protocols 0 and 1, copyreg._reduce_ex explicitly raises this error if it cannot access dict or the object state [1][3]. 3. Legacy Constraints: These protocols are limited and do not support many modern Python features [5][6]. Because copyreg._reduce_ex is designed for compatibility with older implementations, it often lacks the flexibility to handle complex, modern class structures, leading to serialization failures [1][4]. If you encounter this error, the most effective solution is to avoid protocols 0 and 1 entirely in favor of higher-performance, modern protocols (e.g., protocol 5, which is the default in Python 3.14) [5][6]. If you must support older protocols for legacy data, you may need to manually implement reduce_ex or reduce in your class to provide a clear, safe way for the pickler to reconstruct your object [5][7]. As a last resort, if you are attempting to unpickle data generated by an older version of your code, you may need to use a temporary migration script in an environment matching the original pickle time to unpickle the data and re-serialize it using a newer, more robust protocol [4].

Citations:


🌐 Web query:

"cannot pickle 'posix.DirEntry' object" "copyreg._reduce_ex"

💡 Result:

The error "cannot pickle 'posix.DirEntry' object" occurs because os.DirEntry objects, which are returned by os.scandir, are designed to be short-lived, lightweight iterators that hold system-level file handles or cached state [1][2][3]. They are not intended to be persisted, serialized, or passed between processes via mechanisms like multiprocessing.Pool or pickle [1][2]. When you attempt to pickle such an object, the underlying copyreg module invokes _reduce_ex to determine how to serialize it [4][5]. Because os.DirEntry does not implement the necessary protocols to support pickling, this process fails, raising a TypeError [1][2][4]. To resolve this issue, you must avoid pickling the DirEntry object directly. Instead, extract the specific information you need from the object and serialize those primitive data types (e.g., strings, integers, or dicts) [1][2]. Recommended approach: 1. Extract the attributes you require (such as entry.name, entry.path, or the results of entry.is_dir and entry.stat) [1]. 2. Store this data in a dictionary or a custom class that is picklable [1][6]. 3. If you need to restore the "DirEntry-like" functionality later, you can create a simple wrapper or "PseudoDirEntry" class that accepts these stored values in its constructor to mimic the original object's interface [1][6]. Example of manual serialization: # Instead of: pickle.dumps(entry) # Use: data_to_pickle = { 'name': entry.name, 'path': entry.path, 'is_dir': entry.is_dir, 'stat': entry.stat } serialized_data = pickle.dumps(data_to_pickle) [1]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import copyreg, pickle, types, sys

class Foo(type):
    pass
class DirEntry:
    pass
DirEntry.__module__ = "posix"
DirEntry.__name__ = "DirEntry"
entry = DirEntry()

print("sys.version", sys.version.split()[0])
print("expected copyreg._reduce_ex for protocol 0:", repr(copyreg._reduce_ex(entry, 0)))
print("expected copyreg._reduce_ex for protocol 1:", repr(copyreg._reduce_ex(entry, 1)))

try:
    copyreg._reduce_ex(entry, 2)
except Exception as exc:
    print("copyreg._reduce_ex proto >= 2 exception type:", type(exc).__module__ + "." + type(exc).__name__)
    print("copyreg._reduce_ex proto >= 2 message:", str(exc))
PY

python3 - <<'PY'
import pickle, sys

class Foo(type):
    pass
class DirEntry:
    pass
DirEntry.__module__ = "posix"
DirEntry.__name__ = "DirEntry"
entry = DirEntry()

for proto in range(5):
    try:
        dumps = pickle.dumps(entry, proto)
        print("proto", proto, "dumps bytes", dumps, "len", len(dumps))
    except Exception as exc:
        print("proto", proto, "exception type", type(exc).__module__ + "." + type(exc).__name__)
        print("proto", proto, "message", str(exc))
PY

Repository: youknowone/pyre

Length of output: 1281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== _reduce_ex source =="
python3 - <<'PY'
import copyreg, inspect
print(inspect.getsource(copyreg._reduce_ex))
PY

echo "== objects involved in copyreg._reduce_ex lookup =="
python3 - <<'PY'
import copyreg
print("_new_type", copyreg._new_type)
print("type of builtin new:", callable(type(int.__new__)))
for name in ["object", "Foo", "DirEntry"]:
    obj = eval(name)
    print(f"{name}.__new__ type={type(getattr(obj, '__new__', None))!r}")
    print(f"{name}.__new__.__self__={getattr(getattr(obj, '__new__', None), '__self__', None)!r}")
PY

echo "== inspect pyre reduce_protocol_app.py if available elsewhere =="
fd -a 'reduce_protocol_app.py' . -x sh -c 'echo "--- $1"; wc -l "$1"' sh {}
rg -n "def reduce_1|def reduce_2|HEAP|dir_entry|DirEntry|ScandirIterator|_reduce_ex" pyre pyre/pyre-interpreter pyre/extra_tests 2>/dev/null | head -240

echo "== inspect object reduction implementation in pyre =="
cat -n pyre/extra_tests/parity_tests/bytearray_python314.py | sed -n '33,39p' 2>/dev/null || true
cat -n pyre/extra_tests/snippets/pickle_instances.py | sed -n '1,25p' 2>/dev/null || true

Repository: youknowone/pyre

Length of output: 1749


🌐 Web query:

CPython Python 3.14 source Objects/typeobject.c Object_new_error new_is_unsafe

💡 Result:

In CPython 3.14, the source file Objects/typeobject.c manages type objects and their behavior [1][2]. While there is no single function named Object_new_error or new_is_unsafe, these terms relate to logic within tp_new_wrapper for generating error messages when attempting to instantiate types that cannot be created [3]. Specifically, in CPython 3.14, the function tp_new_wrapper includes logic to handle cases where a type's tp_new slot is NULL [3]. A recent change (gh-78465) updated this logic to improve error messages for instances where cls.new(cls,...) is called on a type that is not instantiable (such as certain builtin or extension types) [3]. When staticbase->tp_new is NULL, the interpreter now raises a TypeError with the message "cannot create '...' instances" instead of suggesting a potentially invalid operation [3]. Prior to this, error messages in this context would state that a method is "not safe," specifically using the format: "%s.new(%s) is not safe, use %s.new" [3]. This "not safe" phrasing remains in other parts of the tp_new_wrapper logic as a fallback for providing guidance when types are not properly compatible for instantiation [3]. These mechanisms are part of the broader system in typeobject.c that enforces type safety, manages inheritance, and ensures that the new protocol is followed correctly [2][4].

Citations:


🌐 Web query:

"cannot pickle 'posix.DirEntry' object" "cpython" "Python 3.14"

💡 Result:

The error "cannot pickle 'posix.DirEntry' object" occurs because os.DirEntry objects, which are returned by os.scandir(), are not designed to be serialized [1][2][3]. These objects are intended to be short-lived and hold internal, low-level data optimized for filesystem traversal; they do not support the pickling protocol [2][4]. This issue commonly arises when passing DirEntry objects to multiprocessing worker functions or attempting to store them in a serialized format [1][5]. To resolve this, you should extract the necessary information from the DirEntry object before passing it or pickling it [1][2][3]. Recommended approaches include: 1. Extract and pass only the required attributes: Pass a simple, picklable data type like the file's path (entry.path), name (entry.name), or a dictionary containing the specific file details you need [1][2]. 2. Use string or path representations: If you only need the file path, os.fspath(entry) or entry.path are efficient ways to obtain a string that can be easily pickled and sent to other processes or threads [4]. 3. Reconstruct the object if necessary: If you must maintain an object-like interface on the receiving end, create a custom "pseudo" class (e.g., a data class or simple class) that holds the data you serialized and mimics the original object's behavior [2][3]. Since os.DirEntry instances are tied to the specific system call and internal state of the scandir iterator, they cannot be reconstructed from scratch without the raw filesystem data they originally held [4]. Converting them to standard types (strings, integers, dictionaries) is the standard practice in Python 3.14 and all earlier versions [2][3].

Citations:


🌐 Web query:

"gh-106529" "cannot pickle 'posix.DirEntry' object"

💡 Result:

The error "cannot pickle 'posix.DirEntry' object" occurs because os.DirEntry objects, which are returned by os.scandir, are designed to be low-level, short-lived, and platform-dependent objects [1][2][3]. They hold system-specific references (such as file descriptors or pointers from readdir) that cannot be serialized for inter-process communication or long-term storage [1][2]. Note that "gh-106529" as cited in your query refers to a CPython internal development issue related to the Tier 2 (uops) interpreter and the implementation of FOR_ITER instructions [4][5][6]; it is unrelated to the pickling of DirEntry objects. To resolve the pickling issue in your application, do not attempt to pickle the DirEntry object itself. Instead, extract and serialize the necessary data (such as the file name or path) as primitive types (strings, dictionaries, etc.) [1][7]. Recommended approach: 1. Extract attributes from the DirEntry object (e.g., entry.path, entry.name, or results from entry.is_dir and entry.stat) before serialization [1][7]. 2. Create a dictionary or a custom class that stores these values, and serialize that structure instead [1][3]. 3. If you need to "reconstruct" a similar object on the receiving end, create a lightweight helper class (a "PseudoDirEntry") that holds the serialized data and mimics the required methods [1][3].

Citations:


🌐 Web query:

"posix.DirEntry" "pickle" "cpython" "112134"

💡 Result:

In Python, os.DirEntry objects (which are implemented as posix.DirEntry on POSIX systems) cannot be pickled, and attempting to do so will raise a TypeError: can't pickle posix.DirEntry objects [1]. This is by design because os.DirEntry objects are intended to be short-lived, performance-optimized wrappers for low-level directory entry data provided by the operating system (e.g., from readdir or FindFirstFile) [2][3][4]. They encapsulate state that is tightly coupled to a specific system call and a specific point in time, and they are not intended for long-term storage or serialization [2][4]. If you need to pass file information across process boundaries or store it, you should extract the necessary attributes (such as path, name, or stat information) into serializable objects like dictionaries or custom data structures [1][2]. Regarding the reference 112134 mentioned in your query: there is no widely known CPython issue 112134 specifically documenting a request to make os.DirEntry picklable, as the inability to pickle them is considered a fundamental consequence of their design rather than a bug. Users encountering this error in environments like multiprocessing.Pool are advised to pass paths or other serializable metadata instead of the DirEntry object itself [1].

Citations:


🌐 Web query:

site:github.com/python/cpython "posix.DirEntry" "pickle" "cpython" "gh-71225"

💡 Result:

The GitHub issue #71225 in the CPython repository, titled "Make os.DirEntry exist," refers to the task of exposing the os.DirEntry class, which was previously internal to the posix module [1]. It does not directly relate to pickling os.DirEntry objects [1]. os.DirEntry objects are deliberately designed not to be picklable [1]. They are intended to hold transient, low-level data retrieved from system calls like readdir() (on POSIX) or FindFirstFile() (on Windows) to provide performance optimizations during directory iteration [1]. Because they represent a specific point-in-time state of a file system entry and rely on this low-level context, they cannot be reconstructed independently [1]. If you need to work with directory entries in a way that requires serialization or persistence, you should use pathlib.Path objects or standard string/bytes path representations instead [1]. You can convert an os.DirEntry object to a string or bytes path using os.fspath() [1].

Citations:


Do not exclude pickle protocols 0 and 1 from parity coverage.

os.DirEntry / posix.DirEntry must refuse pickling on every CPython 3.14 protocol; the protocols 0/1 case is not a legitimate difference. Cover these protocols and assert the same TypeError, and fix the generic static-type reduction path if protocols 0/1 currently diverge from cannot pickle 'posix.DirEntry' 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/dir_entry_uninstantiable_python314.py` around
lines 9 - 11, Extend the parity test for os.DirEntry/posix.DirEntry to include
pickle protocols 0 and 1, asserting TypeError with the same “cannot pickle
'posix.DirEntry' object” behavior as other protocols. Update the generic
static-type reduction path used by copyreg._reduce_ex as needed so these
protocols produce the matching rejection instead of being excluded.

Comment on lines +289 to +306
fn llbc_fingerprint_output(child: std::process::Child) -> Option<String> {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(child.wait_with_output());
});
let output = rx
.recv_timeout(std::time::Duration::from_secs(120))
.ok()?
.ok()?;
if !output.status.success() {
return None;
}
let value = String::from_utf8(output.stdout).ok()?.trim().to_string();
// A bare lowercase sha256 and nothing else; anything else means the driver
// printed something this code does not model.
let is_hash = value.len() == 64 && value.bytes().all(|b| b.is_ascii_hexdigit());
is_hash.then_some(value)
}

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

Terminate and reap the fingerprint child after a timeout.

At Line 291, the helper thread takes ownership of Child. At Line 295, a timeout drops the receiver but does not stop that child. wait_with_output() can then keep the extraction process alive indefinitely.

Keep the child handle available through the deadline. Kill and reap it before this function returns on timeout. The current loop can leave up to one process per LLBC_CRATES entry after Cargo exits.

🤖 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-jit-trace/build.rs` around lines 289 - 306, Update
llbc_fingerprint_output to retain access to the child process until the timeout
outcome is known; on recv_timeout expiration, terminate the child and wait for
it to be reaped before returning None. Preserve the existing successful output
parsing and non-success handling, ensuring every timed-out fingerprint process
is cleaned up.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit a0a5097).
Updated: 2026-08-05T12:54:13.842Z

Files in the reviewed diff
majit/majit-backend-cranelift/src/compiler.rs
majit/majit-backend-dynasm/src/lib.rs
majit/majit-backend-dynasm/src/runner.rs
majit/majit-backend-wasm/src/codegen.rs
majit/majit-backend-wasm/src/lib.rs
majit/majit-metainterp/src/compile.rs
majit/majit-metainterp/src/jitdriver.rs
majit/majit-metainterp/src/lib.rs
majit/majit-metainterp/src/optimizeopt/unroll.rs
majit/majit-metainterp/src/optimizeopt/virtualstate.rs
majit/majit-metainterp/src/pyjitpl.rs
pyre/bench/synth/retrace_accumulator_type_flip.py
pyre/extra_tests/parity_tests/dir_entry_uninstantiable_python314.py
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
pyre/pyre-jit-trace/build.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-wasm-runner/src/main.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:2605 ↔ pypy/module/posix/interp_scandir.py:463 — the new DirEntry.__reduce_ex__ ignores its required protocol argument: calling it with zero or extra arguments returns the pickle error, whereas PyPy’s descriptor requires exactly (self, w_protocol) and raises an arity TypeError.

  • majit/majit-metainterp/src/optimizeopt/unroll.rs:1871 ↔ rpython/jit/metainterp/optimizeopt/unroll.py:238 — Pyre newly abandons a retrace when the body JUMP and preamble LABEL arities differ. Upstream always retargets and submits the JUMP; this is a new functional decline, not an equivalent jump_to_preamble port.

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

  • majit/majit-metainterp/src/compile.rs:5263 ↔ rpython/jit/metainterp/pyjitpl.py:3020 — Pyre filters same-green-key merge points with incompatible argument counts; PyPy asserts the invariant instead. This patch only adds diagnostic slot 54.

  • majit/majit-metainterp/src/pyjitpl.rs:6958 ↔ rpython/jit/metainterp/pyjitpl.py:3059 — Pyre declines to append a retrace merge point if any jump argument lacks an intrinsic type; PyPy appends the complete live-box list unconditionally. This patch only records the pre-existing decline in slot 55.

  • majit/majit-metainterp/src/compile.rs:5386 ↔ rpython/jit/metainterp/pyjitpl.py:3021 — Pyre falls back to header_pc when closing greens are absent; PyPy always compares the actual closing live arguments. This patch only records the pre-existing fallback in slot 56.

4. Structural adaptations

  • majit/majit-metainterp/src/pyjitpl.rs:8043 ↔ rpython/jit/metainterp/compile.py:800 — an expired Rust Weak<JitCellToken> becomes a cancelled compilation instead of PyPy’s assertion. This is a Rust ownership/lifetime adaptation; it must remain unreachable in normal operation.

  • pyre/pyre-jit-trace/build.rs:366 ↔ rpython/translator/driver.py:63 — LLBC fingerprint validation is specific to Pyre’s frozen Charon-artifact pipeline. PyPy’s translation driver operates on its live translation context, so it has no corresponding stale-artifact check.

  • majit/majit-backend-wasm/src/codegen.rs:4855 ↔ rpython/jit/backend/x86/assembler.py:2463 — descriptor-less JUMPs retain a Rust legacy-IR fallback to the last LABEL. Normal descriptor-bearing JUMPs now match PyPy’s token-identity dispatch; the fallback has no RPython equivalent.

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