Visitar URL original
long, jit: rbigint follow-ups — write-barrier and 3.14 surface fixes, and walker specializations for long/int div, mod, pow and divmod by youknowone · Pull Request #814 · youknowone/pyre · GitHub
Skip to content

long, jit: rbigint follow-ups — write-barrier and 3.14 surface fixes, and walker specializations for long/int div, mod, pow and divmod - #814

Merged
youknowone merged 19 commits into
mainfrom
rbigint
Jul 29, 2026
Merged

youknowone merged 19 commits into
mainfrom
rbigint

Conversation

@youknowone

@youknowone youknowone commented Jul 26, 2026 •

Copy link
Copy Markdown
Owner

Follow-up work on the ported rbigint, stacked on the end-to-end port (#761) and the first review round (#781).

Correctness

  • long/math/gc — zero-length signed int.to_bytes, math.lcm argument validation, and the placement write-barrier fallbacks.
  • listobject — adopt the 3.14 negative-cursor exhausted sentinel for list iterators.
  • gc — apply the write barrier in the blackhole allocator's ref setters. Previously a ref stored through the blackhole path could leave an old-gen object pointing into the nursery without recording the edge.
  • long/gc — keep rbigint clones fresh and align the wasm old-gen path.
  • descroperation — divmod was the only numeric binop missing the
    needs_numeric_binop_dispatch override gate, so a subclass overriding
    __divmod__/__rdivmod__ was skipped. This is interpreter-level and
    reproduces with PYRE_NO_JIT=1.

Walker specializations

  • long op int now takes the rbigint int_* machine-word legs instead of
    wrapping the W_IntObject operand into a bigint first.

  • New walker arms for long // int, long % int, and long ** int.

  • UNPACK_SEQUENCE folds for the object-pair specialised tuple.

  • divmod(int, int) is specialized on two exact ints. It was previously an
    opaque bh_call_fn may-force for every operand type: divmod(a, b) ran
    3.4x slower than CPython 3.14 while pyre's own a // b + a % b was 81x
    faster. The arm emits the makespecialisedtuple2 Cls_ii shape directly
    (specialisedtupleobject.py:169-179) plus the ll_int_py_div /
    ll_int_py_mod legs with the rint.py _ovf_zer domain guards.

    Measured on the int/int loop: 0.405s → 0.004s (101x), i.e. from 3.4x
    slower than CPython 3.14 to 29x faster. The optimized loop contains no
    CallMayForce, ForceToken, or NewWithVtable.

Tests

  • ci — run pyre/extra_tests/parity_tests in the pyre/check.py jobs; they
    were not previously gated.
  • New parity tests: long_int_div_mod_jit.py, long_int_pow_jit.py,
    rbigint_mixed_int_jit.py, unpack_specialised_pair_shapes.py,
    list_iterator_surface.py, math_lcm_argument_validation.py, plus
    divmod_int_jit.py.
  • rbigint_differential.rs gains the int_* family.

Verification

Measured at this branch tip with a freshly extracted build/llbc census and
both backend binaries rebuilt against it, using python3.14 as the oracle
(PYRE_CHECK_PYTHON3=python3.14):

test dynasm cranelift without JIT pre-existing?
long_int_div_mod_jit.py FAIL OK OK no — added by this branch
bytearray_python314.py FAIL FAIL FAIL (139) yes (#560)
memoryview_python314.py FAIL FAIL FAIL (139) yes (#560)
range_python314.py FAIL FAIL FAIL (1) yes (#560)
slice_python314.py FAIL FAIL FAIL (1) yes (#560)
set_iterator_python314.py FAIL FAIL FAIL (1) yes (#629)

Everything else in the parity suite is green on both backends.

An earlier check.py --backend dynasm,cranelift → 326/326 run should be
disregarded
: it was measured against a stale build/llbc/*.ullbc. cargo build
does not re-extract LLBC, so that run validated traces built from a census
extracted hours earlier from older sources.

The five pre-existing failures

These are not regressions from this branch. All five fail identically on both
backends and under PYRE_NO_JIT=1, so they are interpreter-level, and the
tests were added in #560/#629 long before this work. They surface here only
because this branch's ci: run pyre/extra_tests/parity_tests in the pyre/check.py jobs commit gates them for the first time.

range/slice/set_iterator are the same shape — calling an unbound method
descriptor with no argument reports TypeError: count() missing 1 required positional argument instead of CPython's unbound method ... needs an argument. bytearray/memoryview segfault (139).

They need fixing (or the gate needs to land separately), but they are
independent of the rbigint work.

Branch defect — blocks merge

long_int_div_mod_jit.py, added by this branch, aborts under pyre-dynasm:

GC BUG: invalid type_id=... at obj_addr=... site=copy_nursery_object
  MiniMarkGC::copy_nursery_object <- trace_and_update_object
  <- do_collect_nursery <- alloc_with_type <- dynasm_nursery_slowpath

Confirmed present at this branch tip by a control run (committed tip, clean
working tree, LLBC re-extracted so the census matches the sources): 6/6. It was
masked earlier only by the stale census.

Characterisation

  • dynasm only — cranelift passes, consistent with dynasm_nursery_slowpath
    on the stack. (Note cranelift's shim never collects at allocation sites, so it
    simply never exercises this path; dynasm is the orthodox one here.)
  • JIT-only — passes under PYRE_NO_JIT=1.
  • Cumulative and geometry-dependent — the failing function passes in
    isolation; it needs ~5 earlier allocation-heavy functions. Shrinking the
    nursery (PYPY_GC_NURSERY 128K–512K) does not reproduce it; it depends on
    the default 4 MB fill geometry.

Root-cause analysis so far

An instrumented collector (probe naming the referrer, skipping forwarded
headers) gives:

bad child via varsize_item  parent_type_id=9 (PY_OBJECT_ARRAY) parent=OLD-GEN
capacity=8, item[0]=<nursery addr>  BAD-TID
item[1..7]=<low nursery addrs>      valid
child FORWARDED_EARLIER_TO=None

The parent is an old-gen ItemsBlock; only item[0] is corrupt. It holds the
pre-collection address of an object that a forwarding-history probe shows was
never forwarded by any collection in the whole run — i.e. the object was
unreachable from every GC root when its collection ran, yet compiled code kept
using its address afterwards and stored it into a freshly allocated block.

So this is a missing-GC-root defect, not a write-barrier or zeroing defect.
Some holder keeps a PyObjectRef across a collection without the GC knowing.

Hypotheses refuted with evidence (recorded so they are not re-tried)

hypothesis how it was refuted
malloc_zero_filled misconfigured both backends set it false, citing incminimark.py:211. Only the doc comment at rewrite.rs:140-150 is stale (it claims Nursery::reset() zeroes; the real reset() zeroes on wasm32 only)
uninitialized capacity tail allocation helpers NULL-fill len..cap; poison mode would raise the dedicated poison panic instead
wb_covered drops a needed barrier dumped compiled code: CondCallGcWbArray is emitted before every GcStoreIndexed on the append arm
born-old-gen block missing TRACK_YOUNG_PTRS finish_alloc_in_oldgen sets it; and the block is 72 bytes, far below large_object_threshold
card marking incoherent HAS_CARDS is only ever set by tests, so production degenerates to whole-object remembering (correct)
old-gen spill from alloc_with_type (collector.rs:868-871) breaking the rewrite's WB elision instrumented: ZERO spills in a failing run. This was the leading hypothesis; it is empirically dead for this crash
malloc slow-path register handling x2–x13/x19–x22 are spilled and reloaded from GC-updated slots; regalloc matches aarch64/regalloc.py:958 (pyre spills more)
baked Ref constants unpinned constants resolve through a traced GcTable (gcreftracer.rs, the RPython GCREFTRACER shape)

Still open

Identifying the specific unrooted holder. The diagnostic patch used above is
kept out of tree.

Separately, two genuine latent divergences were found and are not the cause
here, but should be fixed on their own:

  1. alloc_with_type can return old-gen memory where incminimark's
    collect_and_reserve (incminimark.py:865-930) loops minor collections and
    never does. The rewrite's write-barrier elision for fresh mallocs
    (rewrite.py:911) depends on that young-result guarantee.
  2. emit_write_barrier_fastpath_kind silently emits nothing when the WB descr
    is unavailable (_ => return, both arches), and the aarch64 inline card
    write silently skips non-register indices. Upstream asserts in both cases.

— authored by Claude

Summary by CodeRabbit

  • New Features
    • Added CI parity step running extra parity tests under the same CPython interpreter.
    • Expanded parity coverage (JIT divmod, long div/mod, long pow, unpacking, conversions/edge cases) and added clear completion output.
  • Bug Fixes
    • Improved write-barrier descriptor handling, blackhole write barriers, and GC/object alignment.
    • Strengthened constant loading/identity and integer parsing/formatting behaviors.
    • Enhanced iterator exhaustion/restoration and range/list cursor handling.
  • Tests
    • Added GC-poison option and broadened JIT/GC/translation/numeric differential assertions.

@coderabbitai

coderabbitai Bot commented Jul 26, 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: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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: 7a379cfe-df8e-4d5f-bd78-89b953fe3ce0

📥 Commits

Reviewing files that changed from the base of the PR and between 3343057 and c909b06.

📒 Files selected for processing (87)
  • .github/workflows/pyre-ci.yml
  • majit/majit-backend-dynasm/src/aarch64/assembler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-dynasm/src/x86/assembler.rs
  • majit/majit-backend/src/rd_payload.rs
  • majit/majit-backend/src/resume_guard_descr.rs
  • majit/majit-backend/src/resume_value.rs
  • majit/majit-gc/src/collector.rs
  • majit/majit-gc/src/header.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-gc/src/minimarkpage.rs
  • majit/majit-gc/src/oldgen.rs
  • majit/majit-ir/src/descr.rs
  • majit/majit-ir/src/lib.rs
  • majit/majit-ir/src/op_descr.rs
  • majit/majit-ir/src/value.rs
  • majit/majit-metainterp/src/compile.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/resume.rs
  • majit/majit-translate/docs/design-346-generic-adt-classdef-consistency.md
  • majit/majit-translate/src/annotator/annrpython.rs
  • majit/majit-translate/src/annotator/bookkeeper.rs
  • majit/majit-translate/src/front/bigint_binop.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/src/front/rbigint_call.rs
  • majit/majit-translate/src/translator/rtyper/cutover.rs
  • majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs
  • majit/majit-translate/src/translator/rtyper/pyre_call_registry.rs
  • majit/majit-translate/tests/test_rbigint_mir.rs
  • pyre/extra_tests/parity_tests/bool_python314.py
  • pyre/extra_tests/parity_tests/builtin_function_python314.py
  • pyre/extra_tests/parity_tests/bytearray_python314.py
  • pyre/extra_tests/parity_tests/bytes_surface_python314.py
  • pyre/extra_tests/parity_tests/code_python314.py
  • pyre/extra_tests/parity_tests/dict_surface_python314.py
  • pyre/extra_tests/parity_tests/divmod_int_jit.py
  • pyre/extra_tests/parity_tests/divmod_long_int_jit.py
  • pyre/extra_tests/parity_tests/float_complex_python314.py
  • pyre/extra_tests/parity_tests/function_python314.py
  • pyre/extra_tests/parity_tests/int_python314.py
  • pyre/extra_tests/parity_tests/int_to_bytes_zero_length.py
  • pyre/extra_tests/parity_tests/itertools_compress_python314.py
  • pyre/extra_tests/parity_tests/itertools_count_repeat_python314.py
  • pyre/extra_tests/parity_tests/itertools_predicate_types_python314.py
  • pyre/extra_tests/parity_tests/itertools_starmap_python314.py
  • pyre/extra_tests/parity_tests/list_iterator_surface.py
  • pyre/extra_tests/parity_tests/long_int_div_mod_jit.py
  • pyre/extra_tests/parity_tests/long_int_pow_jit.py
  • pyre/extra_tests/parity_tests/math_lcm_argument_validation.py
  • pyre/extra_tests/parity_tests/object_surface_python314.py
  • pyre/extra_tests/parity_tests/rbigint_mixed_int_jit.py
  • pyre/extra_tests/parity_tests/run.py
  • pyre/extra_tests/parity_tests/str_surface.py
  • pyre/extra_tests/parity_tests/thread_local_and_lock_acquire.py
  • pyre/extra_tests/parity_tests/unpack_specialised_pair_shapes.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/jit_fnaddr.rs
  • pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs
  • pyre/pyre-interpreter/src/module/_io/textio.rs
  • pyre/pyre-interpreter/src/module/_pickle/mod.rs
  • pyre/pyre-interpreter/src/module/_random/mod.rs
  • pyre/pyre-interpreter/src/module/math/interp_math.rs
  • pyre/pyre-interpreter/src/module/struct/mod.rs
  • pyre/pyre-interpreter/src/objspace/descroperation.rs
  • pyre/pyre-interpreter/src/objspace/std/formatting.rs
  • pyre/pyre-interpreter/src/pycode.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-interpreter/src/pyopcode.rs
  • pyre/pyre-interpreter/src/runtime_ops.rs
  • pyre/pyre-interpreter/src/type_methods.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/codewriter.rs
  • pyre/pyre-jit/tests/gc_stress.rs
  • pyre/pyre-object/src/functional.rs
  • pyre/pyre-object/src/listobject.rs
  • pyre/pyre-object/src/longobject.rs
  • pyre/pyre-object/src/rbigint.rs
  • pyre/pyre-object/tests/rbigint_differential.rs

Walkthrough

This PR updates GC tracing and alignment, BigInt rooting and arithmetic, interpreter numeric behavior, translation lowering, JIT specialization, shared constant identity, resume data, and parity-test execution. It adds regression coverage across these runtime and compiler paths.

Changes

Runtime and translation

Layer / File(s) Summary
GC layout, diagnostics, and barriers
majit/majit-gc/*, majit/majit-backend-dynasm/*, pyre/pyre-jit/src/eval.rs
GC alignment, contextual tracing diagnostics, write-barrier descriptors, conservative barrier reporting, and reference-store barriers are updated.
RBigInt and interpreter semantics
pyre/pyre-object/*, pyre/pyre-interpreter/src/*
BigInt rooting, alias-preserving arithmetic, conversions, formatting, iterator sentinels, parsing, math operations, and code-constant identity are revised.
Translation and rtyper lowering
majit/majit-translate/src/*
Opaque struct projections, RBigInt and Constants lowering, residual mappings, block-local typed-variable candidates, rollback restoration, and dead-result analysis are extended.
JIT and resume data
pyre/pyre-jit-trace/src/*, pyre/pyre-jit/src/*, majit/majit-metainterp/src/*, majit/majit-ir/src/*
Integer and divmod specialization, tuple unpacking, shared constant pools, tagged resume sources, and constant materialization are updated.

Parity validation

Layer / File(s) Summary
Parity coverage and CI execution
.github/workflows/pyre-ci.yml, pyre/extra_tests/parity_tests/*
The parity runner gains GC-poison support, new arithmetic/conversion/unpacking tests, iterator coverage, uniform OK sentinels, and CPython CI execution.

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

Possibly related issues

  • youknowone/pyre#205 — The PR advances the issue’s GC, BigInt/JIT, translation, and parity-test objectives.

Possibly related PRs

Poem

A rabbit hops through roots so bright,
BigInts keep their shapes just right.
Constants share a glowing key,
Barriers guard each young ref tree.
“OK!” sings softly from the burrow—
JIT paws race toward tomorrow.

🚥 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 is specific and clearly reflects the main themes: rbigint follow-ups, write-barrier fixes, parity/3.14 surface updates, and new long/int JIT specializations.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rbigint

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

https://github.com/youknowone/pyre/blob/84e1b691434337652a087839718b23ea23cae734/pyre-jit-trace/src/jitcode_dispatch/specialize.rs#L948-L951
P1 Badge Add exact-class guards to long division specialization

When a loop is traced with exact long/int operands and later receives an instance of a subclass overriding __floordiv__, __mod__, or the reflected methods, these guards still pass because subclasses retain the builtin ob_type and only retag w_class. The emitted bigint helper therefore bypasses the interpreter’s numeric-method dispatch and silently returns the builtin result; guard the canonical w_class of both operands before specializing.

AGENTS.md reference: AGENTS.md:L14-L19


https://github.com/youknowone/pyre/blob/84e1b691434337652a087839718b23ea23cae734/pyre-jit-trace/src/jitcode_dispatch/specialize.rs#L1147-L1150
P1 Badge Add exact-class guards to long power specialization

When this trace is recorded with exact operands and subsequently reused with a long or int subclass overriding __pow__/__rpow__, the GuardClass/unbox guards only verify the shared layout ob_type; they do not verify the subclass-sensitive w_class. The specialized jit_bigint_int_pow_nomod call then skips the override entirely, so both operands need canonical w_class guards before this fast path is emitted.

AGENTS.md reference: AGENTS.md:L14-L19

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

@github-actions

github-actions Bot commented Jul 26, 2026 •

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit c909b06).
Updated: 2026-07-29T01:15:40.662Z

Files in the reviewed diff
.github/workflows/pyre-ci.yml
majit/majit-backend-dynasm/src/aarch64/assembler.rs
majit/majit-backend-dynasm/src/runner.rs
majit/majit-backend-dynasm/src/x86/assembler.rs
majit/majit-backend/src/rd_payload.rs
majit/majit-backend/src/resume_guard_descr.rs
majit/majit-backend/src/resume_value.rs
majit/majit-gc/src/collector.rs
majit/majit-gc/src/header.rs
majit/majit-gc/src/lib.rs
majit/majit-gc/src/minimarkpage.rs
majit/majit-gc/src/oldgen.rs
majit/majit-ir/src/descr.rs
majit/majit-ir/src/lib.rs
majit/majit-ir/src/op_descr.rs
majit/majit-ir/src/value.rs
majit/majit-metainterp/src/compile.rs
majit/majit-metainterp/src/pyjitpl.rs
majit/majit-metainterp/src/resume.rs
majit/majit-translate/docs/design-346-generic-adt-classdef-consistency.md
majit/majit-translate/src/annotator/annrpython.rs
majit/majit-translate/src/annotator/bookkeeper.rs
majit/majit-translate/src/front/bigint_binop.rs
majit/majit-translate/src/front/mir.rs
majit/majit-translate/src/front/rbigint_call.rs
majit/majit-translate/src/translator/rtyper/cutover.rs
majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs
majit/majit-translate/src/translator/rtyper/pyre_call_registry.rs
majit/majit-translate/tests/test_rbigint_mir.rs
pyre/extra_tests/parity_tests/bool_python314.py
pyre/extra_tests/parity_tests/builtin_function_python314.py
pyre/extra_tests/parity_tests/bytearray_python314.py
pyre/extra_tests/parity_tests/bytes_surface_python314.py
pyre/extra_tests/parity_tests/code_python314.py
pyre/extra_tests/parity_tests/dict_surface_python314.py
pyre/extra_tests/parity_tests/divmod_int_jit.py
pyre/extra_tests/parity_tests/divmod_long_int_jit.py
pyre/extra_tests/parity_tests/float_complex_python314.py
pyre/extra_tests/parity_tests/function_python314.py
pyre/extra_tests/parity_tests/int_python314.py
pyre/extra_tests/parity_tests/int_to_bytes_zero_length.py
pyre/extra_tests/parity_tests/itertools_compress_python314.py
pyre/extra_tests/parity_tests/itertools_count_repeat_python314.py
pyre/extra_tests/parity_tests/itertools_predicate_types_python314.py
pyre/extra_tests/parity_tests/itertools_starmap_python314.py
pyre/extra_tests/parity_tests/list_iterator_surface.py
pyre/extra_tests/parity_tests/long_int_div_mod_jit.py
pyre/extra_tests/parity_tests/long_int_pow_jit.py
pyre/extra_tests/parity_tests/math_lcm_argument_validation.py
pyre/extra_tests/parity_tests/object_surface_python314.py
pyre/extra_tests/parity_tests/rbigint_mixed_int_jit.py
pyre/extra_tests/parity_tests/run.py
pyre/extra_tests/parity_tests/str_surface.py
pyre/extra_tests/parity_tests/thread_local_and_lock_acquire.py
pyre/extra_tests/parity_tests/unpack_specialised_pair_shapes.py
pyre/pyre-interpreter/src/baseobjspace.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/function.rs
pyre/pyre-interpreter/src/jit_fnaddr.rs
pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs
pyre/pyre-interpreter/src/module/_io/textio.rs
pyre/pyre-interpreter/src/module/_pickle/mod.rs
pyre/pyre-interpreter/src/module/_random/mod.rs
pyre/pyre-interpreter/src/module/math/interp_math.rs
pyre/pyre-interpreter/src/module/struct/mod.rs
pyre/pyre-interpreter/src/objspace/descroperation.rs
pyre/pyre-interpreter/src/objspace/std/formatting.rs
pyre/pyre-interpreter/src/pycode.rs
pyre/pyre-interpreter/src/pyframe.rs
pyre/pyre-interpreter/src/pyopcode.rs
pyre/pyre-interpreter/src/runtime_ops.rs
pyre/pyre-interpreter/src/type_methods.rs
pyre/pyre-interpreter/src/typedef.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
pyre/pyre-jit/src/call_jit.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/jit/codewriter.rs
pyre/pyre-jit/tests/gc_stress.rs
pyre/pyre-object/src/functional.rs
pyre/pyre-object/src/listobject.rs
pyre/pyre-object/src/longobject.rs
pyre/pyre-object/src/rbigint.rs
pyre/pyre-object/tests/rbigint_differential.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

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

  • pyre/pyre-interpreter/src/pycode.rs:1660 ↔ pypy/interpreter/pyframe.py:98: LIVE_CODE_WRAPPERS is a TLS HashMap<raw CodeObject*, PyCode*>; PyPy keeps globals on each live frame (FrameDebugData when globals differ, otherwise that frame’s pycode.w_globals). A thread-local, first-writer registry cannot represent another thread’s frame or preserve per-frame namespace identity. This code was already present in upstream/main, so it is not introduced by this patch.

4. Structural adaptations

  • pyre/pyre-interpreter/src/pycode.rs:215 ↔ pypy/interpreter/pycode.py:126: PyPy receives eagerly wrapped co_consts_w; pyre stores compiler ConstantData and lazily publishes boxed constants through AtomicPtr slots. This is a CPython-compatible compiler/free-threading adaptation; the patch preserves the required per-slot identity after publication.

  • pyre/pyre-interpreter/src/pycode.rs:238 ↔ pypy/interpreter/pycode.py:93: pyre keeps immortal Box-allocated code wrappers in a process-global root registry, whereas PyPy’s GC-managed PyCode fields are traced normally. This is required by the Rust/GC ownership model.

  • pyre/pyre-object/src/rbigint.rs:4472 ↔ rpython/rlib/rbigint.py:3030: the formatter-parts cache uses Mutex<Arc<Vec<Arc<RBigInt>>>> plus explicit GC roots instead of PyPy’s ordinary translated list. The shared process-global owner and monotonic cache contents match; synchronization and rooting are free-threading/Rust adaptations.

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

ℹ️ 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-jit-trace/src/jitcode_dispatch/residual_call.rs
Comment thread .github/workflows/pyre-ci.yml

@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: 4a4e91ef8a

ℹ️ 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/eval.rs Outdated
Comment thread pyre/pyre-interpreter/src/module/_io/textio.rs Outdated

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pyre/pyre-interpreter/src/objspace/descroperation.rs (1)

1925-1944: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Raise OverflowError for repeat counts that do not fit isize.

None if big.get_sign() < 0 => Ok(0) makes huge negative repeat factors return [], but CPython sequence repeats raise OverflowError: cannot fit 'int' into an index-sized integer before zero-clamping when the coercion itself overflows. Only repeat counts that successfully convert to a negative isize should clamp to 0. Update test_long_repeat_count_bounds accordingly.

🐛 Proposed fix
         match big.to_isize() {
             Some(v) => Ok(if v < 0 { 0 } else { v as usize }),
-            None if big.get_sign() < 0 => Ok(0),
             None => Err(PyError::new(
🤖 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/objspace/descroperation.rs` around lines 1925 -
1944, Update repeat_count so every long integer that cannot convert to isize,
including huge negative values, returns the existing OverflowError; only
successfully converted negative isize values should clamp to zero. Remove the
sign-based overflow exception in repeat_count and update
test_long_repeat_count_bounds to expect OverflowError for out-of-range negative
repeat counts.
majit/majit-translate/src/front/mir.rs (1)

7715-7730: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add the RBigInt receiver guard for this lshift retarget.

lshift_count_residual_path only validates objspace::descroperation::bigint_lshift_count/bigint_lshift_int_int_result, not that the first argument is an RBigInt value. This arm should keep the existing first_arg_ty.as_ref().is_some_and(|ty| tyref_is_rbigint(ty, self.llbc)) guard before switching to the pointer-ABI residual, matching the surrounding operation retargets.

🤖 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-translate/src/front/mir.rs` around lines 7715 - 7730, Add the
existing RBigInt receiver-type guard to the lshift retarget condition before
calling lshift_count_residual_path, using first_arg_ty.as_ref().is_some_and(|ty|
tyref_is_rbigint(ty, self.llbc)). Only construct the residual OpKind::Call when
both the two-argument shape and receiver guard pass; otherwise preserve op_kind
unchanged.
🤖 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 @.github/workflows/pyre-ci.yml:
- Around line 386-394: Fix the underlying missing-GC-root defect reported by
long_int_div_mod_jit.py in the dynasm runtime before enabling the mandatory
parity_tests gate. Update the relevant dynasm implementation so run.py passes
under the release pyre-dynasm backend, and do not exclude or skip the failing
test.

In `@majit/majit-gc/src/minimarkpage.rs`:
- Around line 12-16: Ensure ArenaCollection::new validates that the PageHeader
size is a multiple of OBJECT_ALIGN before allocating pages, using the existing
header-size and alignment symbols; add an assertion for hdrsize % OBJECT_ALIGN
== 0 while preserving the current allocation behavior.

In `@majit/majit-translate/src/front/mir.rs`:
- Around line 1479-1492: Change the bare-leaf “Constants” metadata registrations
in the constants setup to use entry-or-insert semantics, preventing them from
overwriting workspace declarations published by the TypeDeclKind::Struct
handling. Apply this to the bare key in struct_fields.fields and struct_origins,
while keeping unconditional insertion for the qualified compiler_constants_path
and bytecode::Constants keys.
- Around line 1495-1529: Update the Complex metadata block to use
strip_crate_prefix(num_complex_path) consistently for struct_field_attrs keys
and StructId creation, matching the identities used by the struct arm,
FORCE_ATTRIBUTES_INTO_CLASSES consumers, and resolve_adt_field. Preserve the
existing field rows, aliases, known-name registration, and origin metadata while
ensuring both attribute typing and layout ownership use the crate-stripped
“Complex” spelling.

In `@majit/majit-translate/src/translator/rtyper/cutover.rs`:
- Around line 916-926: Update terminal-block read seeding in the logic around
entry_defs so start, return, and except blocks seed read_nodes from every
input_nodes column, not only defs.values() representatives. Preserve the
deduplicated defs map for identity tracking, but ensure duplicated inputarg
columns such as the exceptblock [etype, evalue] pair are all treated as reads
during backward propagation.
- Around line 430-476: Optimize SubjectSessionSnapshot annotation capture so it
does not iterate and clone definitions for every annotated block on every
subject. Update capture_inner and its callers, including the flow around
dual_gate_check_with_registry, to capture only blocks the current subject can
touch, using the enter_added_blocks_scope tracker, or defer annotation snapshot
creation until a failing subject requires it. Preserve capture_fixed_only
behavior and retain complete snapshots for blocks actually needed for rollback.

In `@majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs`:
- Around line 861-864: Make recognized Array aggregate constructors fail loudly
when array element extraction fails, instead of falling through to
SyntheticTransparentCtor emission. Update the array reconstruction path used by
translate_op and array_aggregate_elements to return or propagate a TyperError
for missing, non-contiguous, duplicate, const, or non-__pos_ writes, keeping
op_canraise consistent with emitted behavior.

In `@majit/majit-translate/tests/test_rbigint_mir.rs`:
- Around line 1220-1224: Extend the module path lists passed to
build_semantic_program_from_llbcs_with_static_addrs_and_module_paths in both
affected test call sites to include baseobjspace, builtins, typedef,
type_methods, and objspace::std::formatting alongside the existing prefixes.
Keep both callers’ coverage consistent so every function referenced through
program.functions is retained by should_lower_module.

In `@pyre/pyre-interpreter/src/baseobjspace.rs`:
- Around line 2484-2494: Update range_iter_reduce_method and
long_range_iter_reduce_method to root every wrapped argument across subsequent
allocations: create a shadow root scope with push_roots(), pin each result
immediately after w_int_new or range_bigint_to_obj, and pass the rooted values
back from the shadow stack to w_range_new or the equivalent constructor. Also
pin w_range before w_tuple_new so it remains rooted during tuple allocation,
following the established pattern in range_compute_slice.
- Around line 1966-1974: Root the range length immediately after converting it
in range_compute_slice, and pass the rooted value into
compute_slice_indices3_big so it remains valid across space_index calls that may
execute Python and collect. Update subsequent length dereferences to use the
rooted representation consistently, alongside sl_start, sl_stop, and sl_step.

In `@pyre/pyre-interpreter/src/objspace/descroperation.rs`:
- Around line 146-157: Unify the duplicated `_divrem` remainder checks by
updating `jit_bigint_divrem_returns_lhs_remainder` to delegate to
`divrem_returns_input_as_remainder`, retaining only its additional sign and
digit-count conditions. Remove the separate digit-indexing predicate logic so
`jit_bigint_mod_floor`, `bigint_mod_core`, and `long_mod` share the same safe
implementation.

In `@pyre/pyre-interpreter/src/pycode.rs`:
- Around line 1416-1454: Change co_consts_w and its initialization/copy paths to
use AtomicPtr<PyObject> elements rather than plain PyObjectRef slots. Update
w_code_fill_consts_from_tuple and w_code_copy_const_slots to publish values with
Release stores, and update w_code_const to use AtomicPtr Acquire loads and
compare_exchange directly, removing the AtomicUsize cast while preserving lazy
realization and canonical-object selection.
- Around line 532-541: Update constants_tuple to preserve the original co_consts
indices: append a fallback value for every null result from w_code_const instead
of skipping it. Reuse the compiler-realized fallback logic used by code_hash,
ensuring the returned tuple length and ordering match code.constants exactly.

In `@pyre/pyre-interpreter/src/pyframe.rs`:
- Around line 3843-3849: Update the Integer arm in pyobject_from_constant to use
compiler_bigint_to_rbigint(value).toint() for int-versus-long selection,
matching load_const_value. Preserve W_IntObject creation for successful
conversions and W_LongObject creation for values that do not fit in i64.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs`:
- Around line 892-912: The operand validation blocks in
try_walker_specialize_binary_op_long_int_div (specialize.rs:892-912) and
try_walker_specialize_binary_op_long_int_pow (specialize.rs:1110-1119) must
decline tagged integer operands before header-dependent checks or value
extraction. In each int/exp validation block, add the existing CAN_BE_TAGGED and
pyre_object::tagged_int::is_tagged_int guard, while preserving the current
non-specialization return behavior.
- Around line 3172-3181: Change the null-allocation branch in the specialized
tuple construction flow around w_specialised_tuple_ii_new so it returns a hard
trace abort/DispatchError rather than None. Propagate that error through both
callers, including simple newtuple and divmod specialization, so previously
recorded IR is discarded wholesale instead of falling back through Ok(None).

In `@pyre/pyre-jit/src/call_jit.rs`:
- Around line 5386-5392: Rename the local variable w_code in the w_code_const
call result to reflect that it stores a constant value rather than a code
object, and update the following cast expression to use the new name.

In `@pyre/pyre-object/src/longobject.rs`:
- Around line 466-487: Add regression coverage for the zero-shortcut aliasing in
jit_bigint_add and jit_w_long_add_raw, asserting that adding zero returns the
exact existing operand pointer rather than allocating a new payload. Include
both operand orders as applicable, and preserve the existing pointer-identity
and immutability expectations covered by
test_bare_bigint_constructor_comparison_and_scalar_residuals.

---

Outside diff comments:
In `@majit/majit-translate/src/front/mir.rs`:
- Around line 7715-7730: Add the existing RBigInt receiver-type guard to the
lshift retarget condition before calling lshift_count_residual_path, using
first_arg_ty.as_ref().is_some_and(|ty| tyref_is_rbigint(ty, self.llbc)). Only
construct the residual OpKind::Call when both the two-argument shape and
receiver guard pass; otherwise preserve op_kind unchanged.

In `@pyre/pyre-interpreter/src/objspace/descroperation.rs`:
- Around line 1925-1944: Update repeat_count so every long integer that cannot
convert to isize, including huge negative values, returns the existing
OverflowError; only successfully converted negative isize values should clamp to
zero. Remove the sign-based overflow exception in repeat_count and update
test_long_repeat_count_bounds to expect OverflowError for out-of-range negative
repeat counts.
🪄 Autofix (Beta)

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: ec1594b6-0538-4a8a-b3d1-2028cafec04f

📥 Commits

Reviewing files that changed from the base of the PR and between 1079fe7 and 4a4e91e.

📒 Files selected for processing (74)
  • .github/workflows/pyre-ci.yml
  • majit/majit-backend-dynasm/src/aarch64/assembler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-dynasm/src/x86/assembler.rs
  • majit/majit-gc/src/collector.rs
  • majit/majit-gc/src/header.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-gc/src/minimarkpage.rs
  • majit/majit-gc/src/oldgen.rs
  • majit/majit-translate/src/annotator/bookkeeper.rs
  • majit/majit-translate/src/front/bigint_binop.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/src/front/rbigint_call.rs
  • majit/majit-translate/src/translator/rtyper/cutover.rs
  • majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs
  • majit/majit-translate/src/translator/rtyper/pyre_call_registry.rs
  • majit/majit-translate/tests/test_rbigint_mir.rs
  • pyre/extra_tests/parity_tests/bool_python314.py
  • pyre/extra_tests/parity_tests/builtin_function_python314.py
  • pyre/extra_tests/parity_tests/bytearray_python314.py
  • pyre/extra_tests/parity_tests/bytes_surface_python314.py
  • pyre/extra_tests/parity_tests/code_python314.py
  • pyre/extra_tests/parity_tests/dict_surface_python314.py
  • pyre/extra_tests/parity_tests/divmod_int_jit.py
  • pyre/extra_tests/parity_tests/divmod_long_int_jit.py
  • pyre/extra_tests/parity_tests/float_complex_python314.py
  • pyre/extra_tests/parity_tests/function_python314.py
  • pyre/extra_tests/parity_tests/int_python314.py
  • pyre/extra_tests/parity_tests/int_to_bytes_zero_length.py
  • pyre/extra_tests/parity_tests/itertools_compress_python314.py
  • pyre/extra_tests/parity_tests/itertools_count_repeat_python314.py
  • pyre/extra_tests/parity_tests/itertools_predicate_types_python314.py
  • pyre/extra_tests/parity_tests/itertools_starmap_python314.py
  • pyre/extra_tests/parity_tests/list_iterator_surface.py
  • pyre/extra_tests/parity_tests/long_int_div_mod_jit.py
  • pyre/extra_tests/parity_tests/long_int_pow_jit.py
  • pyre/extra_tests/parity_tests/math_lcm_argument_validation.py
  • pyre/extra_tests/parity_tests/object_surface_python314.py
  • pyre/extra_tests/parity_tests/rbigint_mixed_int_jit.py
  • pyre/extra_tests/parity_tests/run.py
  • pyre/extra_tests/parity_tests/str_surface.py
  • pyre/extra_tests/parity_tests/unpack_specialised_pair_shapes.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/jit_fnaddr.rs
  • pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs
  • pyre/pyre-interpreter/src/module/_io/textio.rs
  • pyre/pyre-interpreter/src/module/_pickle/mod.rs
  • pyre/pyre-interpreter/src/module/_random/mod.rs
  • pyre/pyre-interpreter/src/module/math/interp_math.rs
  • pyre/pyre-interpreter/src/module/struct/mod.rs
  • pyre/pyre-interpreter/src/objspace/descroperation.rs
  • pyre/pyre-interpreter/src/objspace/std/formatting.rs
  • pyre/pyre-interpreter/src/pycode.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-interpreter/src/pyopcode.rs
  • pyre/pyre-interpreter/src/runtime_ops.rs
  • pyre/pyre-interpreter/src/type_methods.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/codewriter.rs
  • pyre/pyre-jit/tests/gc_stress.rs
  • pyre/pyre-object/src/functional.rs
  • pyre/pyre-object/src/listobject.rs
  • pyre/pyre-object/src/longobject.rs
  • pyre/pyre-object/src/rbigint.rs
  • pyre/pyre-object/tests/rbigint_differential.rs

Comment thread .github/workflows/pyre-ci.yml
Comment thread majit/majit-gc/src/minimarkpage.rs
Comment thread majit/majit-translate/src/front/mir.rs
Comment thread majit/majit-translate/src/front/mir.rs
Comment thread majit/majit-translate/src/translator/rtyper/cutover.rs Outdated
Comment thread pyre/pyre-interpreter/src/pyframe.rs
Comment thread pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
Comment thread pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
Comment thread pyre/pyre-jit/src/call_jit.rs Outdated
Comment thread pyre/pyre-object/src/longobject.rs

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
majit/majit-translate/tests/test_rbigint_mir.rs (1)

1182-1192: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Comment scope no longer matches the list.

jit_bigint_divrem_returns_lhs_remainder is not a _make_int_comparison wrapper, but the comment above claims the whole list is. Either widen the comment or split that path into its own group.

🤖 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-translate/tests/test_rbigint_mir.rs` around lines 1182 - 1192,
Split the path list in the test around the comparison wrappers and
jit_bigint_divrem_returns_lhs_remainder, giving each group a comment that
accurately describes its scope; keep the existing paths and test behavior
unchanged.
majit/majit-translate/src/annotator/annrpython.rs (1)

880-892: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Don't arm snapshot tracking in a pre-committed scope.

The guard here is constructed with committed: true, so the snapshots collected during complete() are never used — but snapshot_block_if_tracking still clones every touched block's variable annotations on each addpendingblock/reflowpendingblock. Leaving the tracker at None (only saving/restoring the outer value) keeps the helper drive allocation-free and matches upstream's finally, which only restores added_blocks.

♻️ Proposed refactor
         let saved = self.added_blocks.borrow_mut().replace(IndexMap::new());
-        let saved_annotation_snapshots = self
-            .subject_annotation_snapshots
-            .borrow_mut()
-            .replace(IndexMap::new());
+        // Pre-committed scope: nothing rolls back, so leave tracking off and
+        // only preserve the caller's tracker across the helper drive.
+        let saved_annotation_snapshots = self.subject_annotation_snapshots.borrow_mut().take();
🤖 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-translate/src/annotator/annrpython.rs` around lines 880 - 892,
Update the pre-committed AddedBlocksGuard setup around complete() so it does not
arm snapshot tracking: leave saved_annotation_snapshots as None while still
preserving and restoring the outer snapshot-tracking value. Keep committed=true
and the existing added_blocks isolation behavior unchanged, matching the
upstream finally flow.
pyre/pyre-interpreter/src/baseobjspace.rs (1)

2453-2469: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead branch: index == -1 is the only value reaching the final else if index < 0, and it is already -1.

index < -1 normalizes first and index >= length cannot hold for a negative index, so the third arm never changes anything. Fold it into the first condition (index < 0) to keep the sentinel rule in one place.

🤖 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/baseobjspace.rs` around lines 2453 - 2469, Update
list_reverse_iter_setstate_method’s index normalization so all negative values
are handled by a single index < 0 branch that assigns the exhausted sentinel -1.
Remove the unreachable final else-if branch while preserving the existing
upper-bound handling for indices at or beyond length.
pyre/pyre-interpreter/src/module/math/interp_math.rs (1)

777-790: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Loop accumulators stay unrooted across the collecting rbigint allocations inside the loop.

n_big is rooted, but numerator, denominator, and i are plain Rust locals held live across n_big.sub(&i) / mul / floordiv, each of which allocates a digit array and can collect. RPython's stack map covers every one of those locals in app_math.comb, so if the rest of this PR's rooting is load-bearing, these need the same RBigIntGcRoot treatment. The product_range recursion in perm (Lines 832-844) and the divisor/value temporaries in lcm have the identical shape.

🤖 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/math/interp_math.rs` around lines 777 - 790,
Root the live bigint accumulators across allocating operations: update
app_math.comb’s numerator, denominator, and i, perm’s product_range recursion
temporaries, and lcm’s divisor/value temporaries to use the established
RBigIntGcRoot treatment, preserving their existing arithmetic and loop behavior.
pyre/pyre-jit/src/eval.rs (1)

10650-10664: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add the write barrier to bh_setarrayitem_gc_r.

bh_setarrayitem_gc_r delegates through bh_setarrayitem_ref_from_descr to the GC-array Ref writer, whose flat path records the store without gc_write_barrier. An array materialized into an existing object needs the same array -> young element edge as the field/interior-field Ref stores.

🤖 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/src/eval.rs` around lines 10650 - 10664, Update
bh_setarrayitem_gc_r to invoke majit_gc::gc_write_barrier for a nonzero array
before delegating to bh_setarrayitem_ref_from_descr, matching the barrier
behavior of bh_setfield_gc_r and bh_setinteriorfield_gc_r while preserving the
existing array-item store path.
🤖 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 @.github/workflows/pyre-ci.yml:
- Around line 392-394: Add a second parity-test invocation alongside the
existing run in the workflow, passing the --gc-poison flag and initially marking
that step continue-on-error. Keep the existing unpoisoned invocation unchanged
and reuse the cpython executable and PYRE_CHECK_PYTHON3 environment setup.

In `@majit/majit-backend/src/resume_value.rs`:
- Around line 57-59: Make ResumeValueSource::Tagged total across all consumers
instead of panicking: in majit/majit-backend/src/resume_value.rs:57-59 add
ResumeValueKind::Tagged or make kind() fallible, and at :91-93 emit a raw-tag
summary. In majit/majit-metainterp/src/resume.rs:2498-2519 make
resolve_materialized_source/resolve_frame_slot_source return the existing
unavailable/zero representation or propagate fallibility; at :3219-3221 align
encode_tagged_source with source_tag’s pass-through behavior; and at :4402-4404
make ResumeDataReader::decode_value degrade to zero or reject Tagged data before
decoding.

In `@majit/majit-gc/src/minimarkpage.rs`:
- Around line 78-82: Update the page-size validation in the relevant minimark
page setup to require alignment to ARENA_ALIGN rather than only WORD, ensuring
allocate_new_page() keeps subsequent GcHeader addresses properly aligned.
Preserve the existing hdrsize alignment assertion and validation behavior for
valid page sizes.

In `@majit/majit-gc/src/oldgen.rs`:
- Around line 110-114: Replace the debug-only alignment check in the allocation
path with a release-enforced validation that rejects or handles any
card_header_bytes value not divisible by OBJECT_ALIGN before
raw.add(card_header_bytes) is used. Preserve the existing allocation behavior
for valid aligned headers and ensure the resulting GcHeader address remains
aligned.

In `@majit/majit-metainterp/src/pyjitpl.rs`:
- Around line 1669-1691: Replace the pool deduplication collection used by
visit_pool with a HashSet<usize>, update the visited parameter and
initialization accordingly, and preserve the existing pointer identity checks
and pool traversal behavior.

In `@majit/majit-translate/src/annotator/annrpython.rs`:
- Around line 387-391: Update BlockAnnotationSnapshot and the snapshot/rollback
logic in the surrounding annotator flow to record whether each snapshotted block
was already present in genpendingblocks when the scope opened. During the
snapshot-driven prune, remove only newly scheduled entries; restore pre-existing
pending entries from the captured snapshot state instead of unconditionally
dropping them, while preserving the later annotated_at_entry pruning behavior.
- Around line 369-392: Update the snapshot restoration block around
subject_annotation_snapshots to use try_borrow/try_borrow_mut for
subject_annotation_snapshots, annotated, all_blocks, blocked_blocks, and
genpendingblocks, matching the best-effort behavior of the adjacent eviction
block. Ensure borrow failures during unwinding are skipped or handled without
triggering another panic, while preserving restoration and cleanup when the
borrows succeed.

In `@majit/majit-translate/src/front/mir.rs`:
- Around line 19688-19694: Update the Complex attrs assertion in the relevant
test to use the canonical key produced by struct_field_attrs—"Complex" after
strip_crate_prefix—rather than "num_complex::Complex". Keep the expected field
attributes unchanged and ensure the ignored test lookup matches the key used by
the implementation.
- Around line 13638-13652: Update the allowlist in the oparg type check around
name_path() to exactly match the products defined by the pinned
rustpython-compiler-core source, excluding multi-field or non-oparg types. Add a
doc comment citing the pinned version and source revision used to verify the
list, while preserving the existing false result for types outside the
allowlist.
- Around line 14698-14709: Update type_arg_splits_per_instantiation so f32 is
included in the DEFERRED exclusions and no longer uses the instantiated-split
default. Align the surrounding comments and tests with f32’s int-register-bank
mapping, preserving the existing split behavior for other supported payload
types.

In `@majit/majit-translate/src/front/rbigint_call.rs`:
- Around line 209-229: Add direct tests in the rbigint call-mapping test module
for both newly supported residual leaves, bigint_int_pow_nomod and
bigint_int_floordiv_nonzero. Exercise each target path through the relevant
mapping function, asserting the expected JIT residual name and ensuring the
surrounding descroperation/objspace predicate is honored.

In `@majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs`:
- Around line 5348-5424: The test function
function_graph_to_flowspace_rebuilds_array_aggregate_as_rpython_list must cover
ordering and fallback behavior: construct the array with out-of-order __pos_
writes and assert newlist arguments remain in index order; add cases for a
missing index, duplicate index, and non-__pos_ FieldWrite on the same base,
asserting the result uses the simple_call constructor and preserves all
FieldWrite operations.

In `@majit/majit-translate/tests/test_rbigint_mir.rs`:
- Around line 863-896: Remove the translated_alias match arm from the
range_bigint_calls assertion in the range_obj_to_bigint test, leaving the
jit_bigint_clone check and its existing failure message intact. Retain the
separate program-wide translated_alias sweep over program.functions.
- Around line 846-852: Update the module path list passed by lower_rbigint to
ShouldLowerModule to include "functional" alongside "rbigint" and "longobject",
so the functional::range_obj_to_bigint graph remains available for the lookup in
test_rbigint_mir.

In `@pyre/extra_tests/parity_tests/list_iterator_surface.py`:
- Around line 95-100: Update the drained reverse-iterator checks around rit2 to
match CPython: expect __reduce__() to retain the source list in the reversed
constructor state, and remove or revise the __setstate__(0)/next assertion so it
reflects actual reversed-iterator behavior rather than list-iterator revival.

In `@pyre/extra_tests/parity_tests/long_int_pow_jit.py`:
- Around line 22-27: Update zero_and_negative so the BIG ** -1 assertion checks
that the result is within a small positive bound instead of comparing round(neg,
70) to 0.0. Preserve the existing type check and ensure the bound reflects the
intended tiny-float behavior.

In `@pyre/pyre-interpreter/src/builtins.rs`:
- Around line 12074-12098: Bound the negative ndigits exponent in the round
handling before constructing the divisor: after deriving the operand magnitude
in the existing is_long/w_int_get_value path, return the integer zero when 10
raised to -nd exceeds the operand’s magnitude, using its bit length to decide
this without materializing the divisor. Only call BigInt::pow for exponents
within that bound, preserving the existing divmod-near behavior otherwise.
- Around line 6796-6819: Update the maxdigits calculation near
NumberStringParser::new to use parser.base for the power-of-two check after
parsing resolves the effective base, while preserving base == 0 handling as
needed. Ensure resolved binary values bypass the limit and resolved decimal
values use int_max_str_digits().

In `@pyre/pyre-interpreter/src/eval.rs`:
- Around line 269-311: Replace the per-walk visited Vec in walk with a
BTreeSet<usize>, updating insertion/duplicate detection while preserving
identity-based cycle prevention and passing the set through recursive calls.
Remove the redundant outer null/is_code guard and invoke walk directly with a
fresh set.

In `@pyre/pyre-interpreter/src/objspace/descroperation.rs`:
- Around line 1038-1045: Remove the operand-aliasing fast paths in the floordiv
and divmod implementations, including the branch around quotient_aliases_a and
the long-divisor branch near bigint_floordiv_nonzero. Always construct a fresh
quotient through the existing bigint division/divmod path, preserving upstream
structural parity and preventing quotient mutation from affecting the input
operand.

In `@pyre/pyre-interpreter/src/objspace/std/formatting.rs`:
- Around line 250-258: Update the negative-value handling in the bigint
formatting path around num.format and the negative flag to remove the prefix
only when the formatted magnitude actually starts with "-". Replace the
debug-only assertion with release-safe validation, preserving the existing
formatting and error behavior while preventing removal of the first digit when
the sign is absent.

In `@pyre/pyre-interpreter/src/pycode.rs`:
- Around line 473-488: Update w_code_copy_const_slots to copy only source
constant slots that have already been published, without calling w_code_const or
otherwise realizing missing constants. Preserve each existing destination slot’s
identity for published values, leave unpublished slots untouched, and continue
marking prebuilt roots dirty after the copy.

In `@pyre/pyre-jit/src/call_jit.rs`:
- Around line 5383-5393: Update bh_load_const_fn to detect when w_code_const
returns PY_NULL before casting and returning the value. For that fallback case,
call load_const_from_code(w_code_ptr, consti) so unresolved constants follow the
blackhole loader behavior; preserve the direct w_code_const result for valid
slots.

In `@pyre/pyre-object/src/rbigint.rs`:
- Around line 4475-4505: Update PendingPartsCacheDigitRoot::new to obtain the
_digits field address using raw-pointer arithmetic, such as
std::ptr::addr_of_mut!, without materializing a &mut RBigInt or mutable field
reference from the shared Arc. Preserve the existing slot cast and
try_gc_add_root registration behavior.

In `@pyre/pyre-object/tests/rbigint_differential.rs`:
- Around line 72-89: Update the machine_word documentation to accurately
describe the distribution, noting that boundary-focused values are selected for
most arms and random words for the remaining arm. In machine_word, add explicit
parentheses around the modulo expressions used as shift amounts in both
power-of-two branches without changing behavior.
- Around line 310-315: Update the zero-divisor assertions in the differential
test’s else branch for int_floordiv, int_mod, int_mod_int_result, and int_divmod
to include the same iteration/context information used by the surrounding
assertions, preserving their existing error expectations.

---

Outside diff comments:
In `@majit/majit-translate/src/annotator/annrpython.rs`:
- Around line 880-892: Update the pre-committed AddedBlocksGuard setup around
complete() so it does not arm snapshot tracking: leave
saved_annotation_snapshots as None while still preserving and restoring the
outer snapshot-tracking value. Keep committed=true and the existing added_blocks
isolation behavior unchanged, matching the upstream finally flow.

In `@majit/majit-translate/tests/test_rbigint_mir.rs`:
- Around line 1182-1192: Split the path list in the test around the comparison
wrappers and jit_bigint_divrem_returns_lhs_remainder, giving each group a
comment that accurately describes its scope; keep the existing paths and test
behavior unchanged.

In `@pyre/pyre-interpreter/src/baseobjspace.rs`:
- Around line 2453-2469: Update list_reverse_iter_setstate_method’s index
normalization so all negative values are handled by a single index < 0 branch
that assigns the exhausted sentinel -1. Remove the unreachable final else-if
branch while preserving the existing upper-bound handling for indices at or
beyond length.

In `@pyre/pyre-interpreter/src/module/math/interp_math.rs`:
- Around line 777-790: Root the live bigint accumulators across allocating
operations: update app_math.comb’s numerator, denominator, and i, perm’s
product_range recursion temporaries, and lcm’s divisor/value temporaries to use
the established RBigIntGcRoot treatment, preserving their existing arithmetic
and loop behavior.

In `@pyre/pyre-jit/src/eval.rs`:
- Around line 10650-10664: Update bh_setarrayitem_gc_r to invoke
majit_gc::gc_write_barrier for a nonzero array before delegating to
bh_setarrayitem_ref_from_descr, matching the barrier behavior of
bh_setfield_gc_r and bh_setinteriorfield_gc_r while preserving the existing
array-item store path.
🪄 Autofix (Beta)

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: 29219a15-1849-4fe3-887d-36e02e4997ff

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4e91e and 3343057.

📒 Files selected for processing (85)
  • .github/workflows/pyre-ci.yml
  • majit/majit-backend-dynasm/src/aarch64/assembler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-dynasm/src/x86/assembler.rs
  • majit/majit-backend/src/rd_payload.rs
  • majit/majit-backend/src/resume_guard_descr.rs
  • majit/majit-backend/src/resume_value.rs
  • majit/majit-gc/src/collector.rs
  • majit/majit-gc/src/header.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-gc/src/minimarkpage.rs
  • majit/majit-gc/src/oldgen.rs
  • majit/majit-ir/src/descr.rs
  • majit/majit-ir/src/lib.rs
  • majit/majit-ir/src/op_descr.rs
  • majit/majit-ir/src/value.rs
  • majit/majit-metainterp/src/compile.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/resume.rs
  • majit/majit-translate/src/annotator/annrpython.rs
  • majit/majit-translate/src/annotator/bookkeeper.rs
  • majit/majit-translate/src/front/bigint_binop.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/src/front/rbigint_call.rs
  • majit/majit-translate/src/translator/rtyper/cutover.rs
  • majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs
  • majit/majit-translate/src/translator/rtyper/pyre_call_registry.rs
  • majit/majit-translate/tests/test_rbigint_mir.rs
  • pyre/extra_tests/parity_tests/bool_python314.py
  • pyre/extra_tests/parity_tests/builtin_function_python314.py
  • pyre/extra_tests/parity_tests/bytearray_python314.py
  • pyre/extra_tests/parity_tests/bytes_surface_python314.py
  • pyre/extra_tests/parity_tests/code_python314.py
  • pyre/extra_tests/parity_tests/dict_surface_python314.py
  • pyre/extra_tests/parity_tests/divmod_int_jit.py
  • pyre/extra_tests/parity_tests/divmod_long_int_jit.py
  • pyre/extra_tests/parity_tests/float_complex_python314.py
  • pyre/extra_tests/parity_tests/function_python314.py
  • pyre/extra_tests/parity_tests/int_python314.py
  • pyre/extra_tests/parity_tests/int_to_bytes_zero_length.py
  • pyre/extra_tests/parity_tests/itertools_compress_python314.py
  • pyre/extra_tests/parity_tests/itertools_count_repeat_python314.py
  • pyre/extra_tests/parity_tests/itertools_predicate_types_python314.py
  • pyre/extra_tests/parity_tests/itertools_starmap_python314.py
  • pyre/extra_tests/parity_tests/list_iterator_surface.py
  • pyre/extra_tests/parity_tests/long_int_div_mod_jit.py
  • pyre/extra_tests/parity_tests/long_int_pow_jit.py
  • pyre/extra_tests/parity_tests/math_lcm_argument_validation.py
  • pyre/extra_tests/parity_tests/object_surface_python314.py
  • pyre/extra_tests/parity_tests/rbigint_mixed_int_jit.py
  • pyre/extra_tests/parity_tests/run.py
  • pyre/extra_tests/parity_tests/str_surface.py
  • pyre/extra_tests/parity_tests/unpack_specialised_pair_shapes.py
  • pyre/pyre-interpreter/src/baseobjspace.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/jit_fnaddr.rs
  • pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs
  • pyre/pyre-interpreter/src/module/_io/textio.rs
  • pyre/pyre-interpreter/src/module/_pickle/mod.rs
  • pyre/pyre-interpreter/src/module/_random/mod.rs
  • pyre/pyre-interpreter/src/module/math/interp_math.rs
  • pyre/pyre-interpreter/src/module/struct/mod.rs
  • pyre/pyre-interpreter/src/objspace/descroperation.rs
  • pyre/pyre-interpreter/src/objspace/std/formatting.rs
  • pyre/pyre-interpreter/src/pycode.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-interpreter/src/pyopcode.rs
  • pyre/pyre-interpreter/src/runtime_ops.rs
  • pyre/pyre-interpreter/src/type_methods.rs
  • pyre/pyre-interpreter/src/typedef.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs
  • pyre/pyre-jit/src/call_jit.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/codewriter.rs
  • pyre/pyre-jit/tests/gc_stress.rs
  • pyre/pyre-object/src/functional.rs
  • pyre/pyre-object/src/listobject.rs
  • pyre/pyre-object/src/longobject.rs
  • pyre/pyre-object/src/rbigint.rs
  • pyre/pyre-object/tests/rbigint_differential.rs

Comment thread .github/workflows/pyre-ci.yml
Comment thread majit/majit-backend/src/resume_value.rs
Comment thread majit/majit-gc/src/minimarkpage.rs
Comment thread majit/majit-gc/src/oldgen.rs
Comment thread majit/majit-metainterp/src/pyjitpl.rs
Comment thread pyre/pyre-interpreter/src/pycode.rs
Comment thread pyre/pyre-jit/src/call_jit.rs
Comment thread pyre/pyre-object/src/rbigint.rs
Comment thread pyre/pyre-object/tests/rbigint_differential.rs Outdated
Comment thread pyre/pyre-object/tests/rbigint_differential.rs
@youknowone
youknowone force-pushed the rbigint branch 2 times, most recently from bdaff58 to 087fb09 Compare July 28, 2026 13:55

@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/087fb0954de5bc38eb1f43713673c90de833a4ba/pyre-interpreter/src/builtins.rs#L6822-L6823
P1 Badge Apply the digit limit after resolving base zero

When int() is called with base 0, this disables the conversion limit before NumberStringParser resolves the actual radix, so a decimal input such as int("9" * 5000, 0) bypasses sys.set_int_max_str_digits() even though the equivalent base-10 call is rejected. Base 0 should inherit the limit when it resolves to a non-power-of-two radix; otherwise applications using the limit to bound untrusted parsing remain exposed to the expensive decimal conversion.

AGENTS.md reference: AGENTS.md:L194-L196


https://github.com/youknowone/pyre/blob/087fb0954de5bc38eb1f43713673c90de833a4ba/pyre-interpreter/src/pycode.rs#L467
P1 Badge Root constants owned by standalone code objects

When code.replace(co_consts=(..., int(dynamic_text), ...)) creates a code object that is held only in an ordinary frame local, this store makes the off-GC PyCode slot the sole owner of the managed long, but mark_prebuilt_roots_dirty() does not add that wrapper to any global walk: walk_raw_code_roots is invoked only for running or function-owned code, while visiting the local code pointer itself does not recurse into a Box-immortal object. A subsequent full collection can therefore reclaim the constant and leave code.co_consts dangling. The fresh evidence beyond the prior nested-code report is that the new recursive walker still has no caller for standalone code wrappers, so these wrappers need registration with a raw-root owner or equivalent tracing.

AGENTS.md reference: AGENTS.md:L194-L196

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

@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/c7e30c03a758cb55e30759c819a7b9a086fb6386/pyre-interpreter/src/eval.rs#L292-L293
P1 Badge Trace constants from every reachable code wrapper

The earlier nested-code case is handled by this recursion, but the current revision still invokes walk_raw_code_roots only for code reached through functions, active frames, or suspended generators. A standalone PyCode retained in an ordinary local, list, dict, or module binding remains Box-immortal, so the collector ignores the wrapper and never executes this loop; after code.co_consts realizes a managed value such as a large integer, a collection can reclaim it while its atomic slot still points to the old address. Make PyCode genuinely GC-managed as in PyPy, or ensure every generic reference to an immortal code wrapper dispatches through this walker.

AGENTS.md reference: AGENTS.md:L194-L196

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

…ion, and placement barrier fallbacks

`rbigint.tobytes` skips its final sign-fit check when `nbytes == 0`
(rbigint.py:450), and `-1` is the one value whose two's complement emits
no bytes, so `(-1).to_bytes(0, "big", signed=True)` returned `b''`.
Reject a zero length for any nonzero value in signed mode at the
`int.to_bytes` seam and leave the ported `tobytes` unchanged.

`math.lcm` returned as soon as the running result reached zero, skipping
`__index__` on the remaining arguments, so `math.lcm(0, 1.5)` returned 0
instead of raising TypeError. Keep converting every argument and
short-circuit only the arithmetic.

`alloc_nursery_typed_with_placement` and
`alloc_nursery_collecting_typed_rooted` left `*needs_write_barrier`
unwritten when no backend hook was installed; write the conservative
`true` their sibling fallbacks report.

Add parity tests for both interpreter behaviors.

Assisted-by: Claude
…t iterators

`listiter_setstate`/`listreviter_setstate` store -1 for a negative state
instead of clamping to zero, and the forward one clamps an over-long
state to the list length. The sentinel keeps the source list referenced,
so a later in-range `__setstate__` revives the iterator, while running
off the end still drops the list for the forward iterator only.

`descr_next`, `__length_hint__` and `__reduce__` now all read a negative
cursor as exhausted: reduce reports the empty-producer form even while
the source list is alive, and the reverse iterator no longer clears its
sequence on exhaustion.

Mirror the same sentinel check in the `runtime_ops` list-iterator fast
paths so the traced FOR_ITER arm agrees with `baseobjspace::next`.

Extend list_iterator_surface.py with the clamp, revive and drain cases.

Assisted-by: Claude
The suite was wired into no workflow, so its CPython-observable parity
baselines — including rbigint_mixed_int_jit.py — never ran in CI. Add a
step to the shared check.py job steps, which already built the release
pyre-dynasm and pyre-cranelift binaries the runner drives.

15 scripts ended with their own status line instead of the "OK" sentinel
run.py requires, so the runner reported them as failures on both the
CPython and the pyre legs; append the sentinel to each.

Assisted-by: Claude
`long_floordiv`, `long_mod` and `long_pow` widened a machine-int operand
with `as_bigint` and ran the generic bigint path, while `long_add`,
`long_sub` and `long_mul` already used the `int_*` legs. longobject.py
dispatches all of them through `_make_descr_binop(_floordiv,
_int_floordiv)` / `(_mod, _int_mod)` and `descr_pow`, which call
`rbigint.int_floordiv`, `rbigint.int_mod_int_result` and
`rbigint.int_pow` on the unwrapped word.

Add the three interpreter seams and their residuals, and retarget the
quotient and pow seams in the MIR front. `int_mod_int_result` returns a
machine int, so it gets no entry in the divmod projection map, whose
results are all modeled as GC references.

Cover the machine-word legs in the differential oracle: int_add/sub/mul,
int_and_/or_/xor, int_floordiv, int_mod, int_mod_int_result and
int_divmod, over a divisor stream weighted towards zero, ±1, powers of
two and the ends of the signed range.

Assisted-by: Claude
`try_walker_specialize_unpack` folded the `unpack_sequence_fn` validator
and its per-index `unpack_item_fn` reads only when the sequence was
`SPECIALISED_TUPLE_II_TYPE`. `makespecialisedtuple2`
(specialisedtupleobject.py:169-179) never falls back to a plain tuple for
an arity-2 pair, so any pair that misses `is_plain_int1` /
`type(w) is W_FloatObject` is a `W_SpecialisedTupleObject_oo` and every
`a, b = pair` over one left three opaque `CALL_MAY_FORCE` residuals in the
loop. `objspace.py:519-523 fixedview` reaches `tolist()` for every
`W_AbstractTupleObject`, and the specialised `tolist`
(specialisedtupleobject.py:58-64) unrolls over the value slots, so the
whole unpack is traced inline upstream.

Add the `oo` arm: guard the class once, then read `value0`/`value1` with
`getfield_gc_r`. `wraps[i]` is the identity for an object slot
(specialisedtupleobject.py:26-27), so the field read is already the item —
no re-boxing and no `may_force` execution to recover a box identity.

The class guard moves into a helper both helper arms call. The item arm
previously relied on its partner validator fold having run; guarding there
too is a no-op once the heap cache knows the class, and proves the layout
when only the item reads fold.

`divmod(long, long)` is the shape this covers: 400k-round `div_classic`
carries one `CALL_MAY_FORCE` per iteration instead of four. An
unpack-dominated loop over an object pair goes 0.32s -> 0.04s.
`PyreBlackholeAllocator::bh_setfield_gc_r` stored the reference with a raw
pointer write and no write barrier, and `bh_setfield_gc_r` on the backend
(`runner.rs`) — which does emit one — is shadowed by this override.
`allocate_with_vtable` routes a non-int/float virtual to
`bh_new_with_vtable`, which allocates in the non-moving old generation, so
resume materialization produced an `old -> young` edge that never entered
`old_objects_pointing_to_young`. The minor collection then reclaimed the
young payload while the materialized struct still referenced it.

Measured on `rbigint_mixed_int_jit.py` under `MAJIT_GC_NURSERY_POISON=1`:
an old-gen `W_LongObject` (`alloc_site=bh_new_with_vtable`) reached the
nursery-reset check with `value` pointing into the reset nursery,
`TRACK_YOUNG_PTRS` still set and the barrier entry point never called for
it. The run aborted at `PYPY_GC_NURSERY=128k/512k/1m/4m` and now passes at
all four.

`bh_setinteriorfield_gc_r` delegated to the `_i` variant and had the same
gap; it now takes the barrier on the array base, as the backend's
implementation does.

`run.py` gains `--gc-poison`, which sets `MAJIT_GC_NURSERY_POISON=1` for
the pyre backends: without poison a reclaimed nursery object's stale header
usually still decodes as a valid type id, so the corruption is silent.

Assisted-by: Claude
`_make_descr_binop` (longobject.py:424,441) routes a `W_IntObject` right
operand to `_int_floordiv` / `_int_mod`, which keep the divisor a machine
word. The walker had no arm for that pair, so both ops recorded an opaque
`CallMayForceR` with its force token, `GuardNotForced` and heap flush.

`try_walker_specialize_binary_op_long_int_div` records the two legs with
their distinct result representations: `_int_floordiv` produces a bigint
quotient boxed inline as a `W_LongObject`, while `_int_mod` calls
`rbigint.int_mod_int_result` and boxes a plain `W_IntObject` — the
remainder of a long by a machine int always fits a machine word, so that
leg allocates no result bigint.

`long_floordiv` / `long_mod` raise `ZeroDivisionError` before reaching the
`_nonzero` rbigint seam, so the divisor test is recorded as
`GUARD_TRUE(int_ne(divisor, 0))`; a zero divisor on replay bails to the
interpreter rather than re-deriving the message in the trace.

Measured on a floordiv/mod loop (5M iterations, startup-subtracted,
interleaved, min of 5): 0.812s -> 0.264s (3.08x). The optimized loop drops
from 2 `CallMayForceR` per iteration to `CallI` + `CallR` + `CallI`, with
no `ForceToken` / `GuardNotForced` and no `NewWithVtable` for the quotient.
Against CPython 3.14 the same loop goes from 3.5x slower to 1.25x.

`long_int_div_mod_jit.py` covers the four sign combinations, result types,
bool operands, a zero divisor reached after the trace is warm, in-place
forms, the reflected `int // long` order, a quotient that fits a machine
int, an alternating-divisor site, and an int-subclass divisor.

Assisted-by: Claude
`descr_pow` (longobject.py:206-231) keeps a `W_IntObject` exponent unwrapped
and calls `rbigint.int_pow`; only a long exponent reaches `rbigint.pow`.
The walker had no arm for that pair, so `**` recorded an opaque
`CallMayForceR` with its force token, `GuardNotForced` and heap flush.

`long_pow` reaches the `int_pow` call past four short-circuits. The exponent
tests unbox to a machine word and record as `GUARD_TRUE(int_gt(exp, 0))`.
The three base tests (0 / 1 / -1) collapse into a single
`GUARD_FALSE(jit_bigint_fits_int(base))`: a base that does not fit a machine
word has magnitude at least `2**63`, so it is none of those three. That is
strictly stronger than the interpreter's comparisons — it can only bail more
often — and it costs one call instead of three bigint comparisons against
baked constants. It also makes the trailing result-fits guard the floordiv
and shift legs carry redundant here, so this arm emits none.

Measured on `base ** 3` (300k iterations, startup-subtracted, interleaved,
min of 5): the optimized loop loses its `CallMayForceR` entirely, 0.270s ->
0.240s (1.12x); a pow-only loop with no other operator goes 0.248s ->
0.232s. The remaining 6.3x against CPython 3.14 on that loop is inside
`rbigint.int_pow` itself, not the trace shape — the pure-pow loop already
records zero may-force calls.

`long_int_pow_jit.py` covers positive exponents for both signs of base, the
zero-exponent and negative-exponent short-circuits, bases small enough to
stay off the fast arm, a bool exponent, an alternating-exponent site, the
in-place form, and three-argument `pow`.

Assisted-by: Claude
`divmod(a, b)` arrived as an opaque `bh_call_fn` residual — one
`CallMayForceR` with `ForceToken` / `GuardNotForced` / `GuardNoException`
plus two `NewWithVtable` operand boxes per call.  Recognize the builtin in
the walker and emit `guard_value(callable)` + `guard_class` + exact
`w_class` guards + the shared rint.py `_ovf_zer` domain guards + the
`OS_INT_PY_DIV` / `OS_INT_PY_MOD` elidable calls into a virtual `Cls_ii`
specialised tuple, which the partner unpack fold then reads through.

The `_ovf_zer` guard pair and the `int_py_div` / `int_py_mod` call are
extracted from the int `BINARY_OP` arm so both sites share them, and the
`Cls_ii` construction is extracted from `try_walker_specialize_newtuple`.

`descroperation::divmod` also lacked the `needs_numeric_binop_dispatch`
gate every other numeric binop carries, so an `int` subclass overriding
`__divmod__` took the numeric fast path and the override was ignored
(reproducible with `PYRE_NO_JIT=1`).

Measured on a 3M-iteration `q, r = divmod(i + 1234567, 60)` loop:
0.405s -> 0.004s, matching the separate `//` + `%` loop.  `long // int`
divmod is unchanged; its residual returns a pair and has no arm yet.

Assisted-by: Claude
…d GC

`emit_write_barrier_fastpath_kind`, the malloc slow path and
`emit_setarrayitem_gc_write_barrier` read the descriptor through
`with_dynasm_active_gc`, which yields `None` when compilation runs off the
thread owning `DYNASM_ACTIVE_GC`; both arches then emitted no barrier.
Route those call sites through `dynasm_write_barrier_descr`, which falls
back to `WriteBarrierDescr::for_current_gc()` and preserves an explicit
`None` reported by a bound collector.

Add `MiniMarkGC::get_write_barrier_descr`, zeroing the card fields when
`card_page_shift` is 0, with a unit test over the card geometry.

Replace the `validate_type_id` call in `copy_nursery_object` and the
type-id check in `grey_child` with panics that also report the holder
address, its type id, the slot offset within it, the nursery bounds and
the first eight holder words, threading the tracing site and holder
through every minor- and major-collection call site.

Assisted-by: Claude

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3517af753

ℹ️ 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/pycode.rs
…igint payloads

PyCode fills `co_consts_w` from the constants tuple and hands out that one
object per index: `w_code_co_const` becomes `w_code_const`, publishing the
first realization atomically, and `load_const_pyobj`, `code_getdocstring`
and the walker's `loadconst_operand_ref` read through it instead of
realizing a fresh wrapper per load.  The `PyCode` root forwarder now visits
every filled `co_consts_w` slot, so a managed `W_LongObject` constant held
by a Box-immortal code object is reachable.  `code_constant_at` becomes
`constant_at`, taking a `ConstIdx` and covering every constant kind.

`clone()` on a stored rbigint is replaced by `translated_alias()` where
source translation aliases the payload rather than copying it, and
`w_long_from_raw` / `w_long_get_raw_value` let an int subtype wrapper and
`descr_abs` share the operand payload.  rbigint gains `RBigIntHandle` with
`Deref`/`DerefMut`/`Drop`, a checked `try_new`, `_float_ldexp`, and a
`Result` return on `_full_digits_lshift_then_or`.

`descroperation` bigint helpers take `&BigInt` instead of consuming their
operands, and gain `jit_bigint_int_add`, `jit_bigint_int_sub`,
`jit_bigint_int_mul`, `jit_bigint_lshift_int_int_result`,
`jit_bigint_divrem_returns_lhs_remainder`, `jit_bigint_invert` and
`jit_float_fmod` as residual entry points.  Callers in `_random`, `_ctypes`,
`_io.textio`, `struct`, `_pickle`, `math`, `formatting`, `type_methods`,
`typedef`, `builtins` and `functional` move off the num-bigint surface
(`sign()`, `to_u64()`, `to_str_radix`, `parse_bytes`) onto the rbigint
operations, and `format_int_radix` propagates `RBigIntError::Memory`.

`W_ListObject::object_grow` emits the owner write barrier after installing
the fresh items block, inside the residualized helper rather than at the
outer append.

majit-translate lowers the rbigint residual shapes in the MIR front end:
`digits_from_raw_parts`, `translated_alias`, the fallible `toint`, and the
constants-wrapper `deref`/`index` reads, with rtyper cutover and flowspace
adapter support.

Tests: two rbigint gc_stress programs, a huge-constant identity check in
`code_python314.py`, a new `divmod_long_int_jit.py` parity test, and
`test_rbigint_mir.rs` coverage for the new lowerings.

Assisted-by: Claude
`emit_wb_helper_call_x86` saves xmm0-xmm15 to the stack with `MOVAPS`,
which requires a 16-byte-aligned address.  The trace body runs at
0-mod-16, the nine GPR pushes ahead of the save leave `rsp` at 8-mod-16,
and `SUB rsp, 256` preserves that, so every one of those stores targets
an odd 8-byte boundary and raises #GP.  Use `MOVUPS` for the save and the
matching restore; the alignment compensation in
`emit_abi_call_rax_after_one_push` applies only to the later CALL.

`_build_wb_slowpath` saves the XMM registers with `MOVSD_bx`, which has
no alignment requirement, as does this backend's own
`push_all_regs_to_jitframe_raw`.

The sequence was unreachable until `dynasm_write_barrier_descr` gained
its fallback: `emit_write_barrier_fastpath_kind` returned early whenever
compilation ran off the thread owning `DYNASM_ACTIVE_GC`, so no barrier
was emitted at all.

Assisted-by: Claude
`compress.__doc__` kept the 3.14.5 line break ("... using the selectors
to\nchoose ..."); 3.14.6 breaks one word earlier ("... using the
selectors\nto choose ...").  Take the 3.14.6 text.

`itertools_compress_python314.py` compared the docstring against a
literal carrying the old break, so the reference interpreter failed its
own parity test on a 3.14.6 runner.  Compare with the line breaks
collapsed, which still pins the wording.

Assisted-by: Claude
The shift, `long op long`, `long / long`, `long <cmp> int` and
`long <cmp> long` legs bypass special-method lookup but guarded only
`guard_class` on `ob_type`.  `allocate_instance` gives an `int` subclass
its own RPython class, so one `guard_class` separates it from
`W_IntObject`; pyre shares the `ob_type` layout between the two and
carries the Python-visible class in `w_class`, so `guard_class` alone let
a subclass reuse the trace and skip its `__lshift__` / `__add__` /
`__truediv__` / `__lt__` override.

Resolve each operand's canonical class through
`walker_exact_builtin_class` and emit `walker_guard_exact_w_class`
alongside the existing class guard, as the base, `//`/`%` and `**` legs
already do.  The shift and `long <cmp> int` legs checked exactness at
record time only; `long op long`, `long / long` and `long <cmp> long` had
no exactness check at all.

Assisted-by: Claude
`thread_local_and_lock_acquire.py` asserted `TIMEOUT_MAX == 9223372036.0`
and fed `9223372036.854` to `acquire`.  Both are the POSIX bound: CPython
computes `TIMEOUT_MAX` as `floor(min(PY_TIMEOUT_MAX * 1e-6, PyTime_MAX in
seconds))`, and `PY_TIMEOUT_MAX` on Windows is the DWORD-millisecond
limit of its wait API, so the reference interpreter failed this test on a
Windows runner.

Take the bound from `_thread.TIMEOUT_MAX` and assert what holds on every
platform: it is a positive whole-second float, and `acquire` accepts it.
The `OverflowError` cases keep their literals — those come from the
int64-nanosecond timestamp conversion, which runs before the timeout
bound check and does not vary by platform.

Assisted-by: Claude
@youknowone
youknowone merged commit 3c4f410 into main Jul 29, 2026
23 of 24 checks passed
@youknowone
youknowone deleted the rbigint branch July 29, 2026 03:03
youknowone added a commit that referenced this pull request Jul 29, 2026
…ger banks

`vec_index_regular_leaf` spelled its index-type test inline.  Extract it as
`vec_index_type_is_scalar` and record why both banks are accepted: `usize` —
what essentially every real callsite indexes with — serializes as
`{"UInt": "Usize"}` and types as `Unsigned`, not `Int`, so an `Int`-only test
leaves the fold dead.  A `Range` index types as `Ref` and stays rejected,
which is what the gate exists for.

The acceptance set itself is unchanged: #814 widened it in the same place.
What was missing is a test that runs — the trap was caught only by the
`#[ignore]`d real-LLBC anchor `vec_index_mut_fill_user_function_args_real`,
which needs the 440MB corpus and does not run in CI.  The new unit test pins
both banks and the `Range` rejection off a JSON fixture.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jul 29, 2026
…ger banks

`vec_index_regular_leaf` spelled its index-type test inline.  Extract it as
`vec_index_type_is_scalar` and record why both banks are accepted: `usize` —
what essentially every real callsite indexes with — serializes as
`{"UInt": "Usize"}` and types as `Unsigned`, not `Int`, so an `Int`-only test
leaves the fold dead.  A `Range` index types as `Ref` and stays rejected,
which is what the gate exists for.

The acceptance set itself is unchanged: #814 widened it in the same place.
What was missing is a test that runs — the trap was caught only by the
`#[ignore]`d real-LLBC anchor `vec_index_mut_fill_user_function_args_real`,
which needs the 440MB corpus and does not run in CI.  The new unit test pins
both banks and the `Range` rejection off a JSON fixture.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Jul 29, 2026
…k folds, dyn-indirect on by default (#873)

* majit: attach source_lines context to a mergeinputargs UnionError

`annrpython.py:437-438` sets `e.source = source_lines(graph, block, None,
long=True)` on the UnionError before either recording it under `keepgoing` or
re-raising; the port dropped that step, so the error carried only the two
offending annotations.  `source_lines` is already ported
(`tool/error.rs:400`) and the sibling `KeyError` panic in the same function
already prints slot/block context — only this arm was missing it.

Without it the message does not identify the merge.  The merging block is
routinely an inlined callee, so the graph named by the census subject is not
the graph that failed: `pyre_interpreter::pyopcode::execute_copy` reports
`r_uint ∪ int` but the merge is in `opcode_copy_value`, and the five
`*pop_jump*` subjects all resolve to one block in
`BranchOpcodeHandler::guard_truth_value`.

The failing input slot is threaded out of the `unionof` fold so it can be
named alongside the graph.

Census is unchanged at phaseA 314 / phaseB 24 with an identical histogram —
this alters an error string, not a disposition.

Assisted-by: Claude

* majit: quote the block's operations when a mergeinputargs union fails

`source_lines1` answers `no source!` for every graph lowered from LLBC rather
than from Python, because `graph.source` is absent — which is all of them
here.  So the `source_lines` context added alongside it names the failing
graph but never shows the block, losing the half of upstream's message that
localises the merge (`error.py:34-48` quotes the block's operation line
range).

Quote the block's operations instead, rendered the way `gather_error` already
renders an operation (`tool/error.rs:498`).  Blocks with no operations —
which is what a graph's startblock looks like — say so explicitly, and that
distinction is itself the diagnosis: `pyopcode::u32_as_usize` reports
`int ∪ r_uint` at slot 0 of a block with no operations, i.e. its own entry,
so the two annotations are the caller-supplied ones and nothing in the body
is involved.

Census is unchanged at phaseA 314 / phaseB 24 with a byte-identical
histogram.

Assisted-by: Claude

* pyre: residualize the SYS_MODULES registry read behind a marked accessor

`SYS_MODULES` (`LazyLock<Mutex<HashMap<String, usize>>>`) is the
process-owned name→module registry, distinct from the `SYS_MODULES_DICT`
pointer already residualized beside it.  Its entries are stamped at runtime
by `set_sys_module` / `remove_sys_module`, so it is not a build-time constant
and the front end must not trace into the read.

The two pure readers — `check_sys_modules`'s fallback after the
`sys.modules` dict misses, and `get_interpreter_sys_module` — now share one
`#[majit_macros::dont_look_inside] sys_modules_registry_get(name)` seam with
a `jit_fnaddr` `push_alias_pair` row, the `lookup_exc_class` shape (same
`fn(&str) -> Option<PyObjectRef>` signature).  The mutators and the GC
walkers keep the raw static.

The merged seam is poison-tolerant, which is `get_interpreter_sys_module`'s
existing spelling rather than `check_sys_modules`'s `.unwrap()`: a poisoned
mutex means another thread panicked while holding it, and a
`HashMap<String, usize>` carries no invariant that a panic mid-`insert` or
mid-`remove` could leave broken.

Census, freshly re-extracted corpus.  Default (gate off) is unchanged at
phaseA 314 / phaseB 24 — `SYS_MODULES` contributes no record there.  Under
`PYRE_DYN_INDIRECT=1`, where the dynamic-dispatch bodies reach it, the root
`pyre_interpreter::importing::SYS_MODULES` goes 12 → 0 and distinct roots
52 → 51.  Records stay at 214: ten of the freed graphs re-root one wall
deeper at `std::io::stdio::stderr` and two at `vec::Vec::index`, so this is
again a roots-cleared rather than records-cleared slice.

`python3 pyre/check.py --backend dynasm,cranelift,wasm` — dynasm 328/328,
cranelift 328/328, wasm 325/325.

Assisted-by: Claude

* pyre: residualize the fd-write and thread-identity host seams

Both are host boundaries whose value is produced fresh per call, never a
build-time constant, and each is the single in-tree seam its traced callers
reach the host through:

- `host_seam::emit_stdout` / `emit_stderr` — the fd 1 / fd 2 writers that
  tracebacks, the warning machinery (`warn` -> `warn_category` ->
  `emit_stderr`) and the displayhook all funnel through.  Marked in both
  spellings: the unix bodies in `host_seam.rs` and the non-unix mirror
  declared inline in `lib.rs`, since a corpus extracted on a non-unix host
  sees only the latter.
- `module::thread::current_ident` — reads
  `rustpython_host_env::thread::current_thread_id`.

Each takes a `jit_fnaddr` `push_alias_pair` row; `&[u8]` arguments and the
`()` / `i64` returns need no result shell.

Census, freshly re-extracted corpus:

| root | gate off | gate on |
| --- | --- | --- |
| `std::io::stdio::stderr` | 2 -> 0 | 12 -> 0 |
| `rustpython_host_env::thread::current_thread_id` | 4 -> 0 | 12 -> 0 |

Default (gate off) phaseA 314 -> 313, distinct roots 48 -> 46; three of the
freed graphs re-root at `__dyn_call` and one at `saturating_sub`.  Under
`PYRE_DYN_INDIRECT=1` records go 214 -> 199 and roots 51 -> 49, with eleven
re-rooting at `vec::Vec::index` — now the largest single item left on the
ledger that gates the gate flip.

`python3 pyre/check.py --backend dynasm,cranelift,wasm` — dynasm 328/328,
cranelift 328/328, wasm 325/325.  `wasm32-unknown-unknown` target check
clean, which is the configuration that compiles the non-unix mirror.

Assisted-by: Claude

* majit: name the Vec index/index_mut acceptance test and pin both integer banks

`vec_index_regular_leaf` spelled its index-type test inline.  Extract it as
`vec_index_type_is_scalar` and record why both banks are accepted: `usize` —
what essentially every real callsite indexes with — serializes as
`{"UInt": "Usize"}` and types as `Unsigned`, not `Int`, so an `Int`-only test
leaves the fold dead.  A `Range` index types as `Ref` and stays rejected,
which is what the gate exists for.

The acceptance set itself is unchanged: #814 widened it in the same place.
What was missing is a test that runs — the trap was caught only by the
`#[ignore]`d real-LLBC anchor `vec_index_mut_fill_user_function_args_real`,
which needs the 440MB corpus and does not run in CI.  The new unit test pins
both banks and the `Range` rejection off a JSON fixture.

Assisted-by: Claude

* pyre: split try_eq_w into has_eq_w_hook + eq_w_hooked leaves

`try_eq_w` was the only `try_*` hook wrapper in `dict_eq_hook` that read its
thread-local inline.  Its two siblings, `try_hash_w` and `try_hash_str`, both
decompose into a marked `has_*_hook` predicate plus a marked `*_hooked`
trampoline returning a sentinel, leaving the `Option` construction visible to
the front end.  `try_eq_w` now does the same: `has_eq_w_hook` /
`eq_w_hooked` carry the `dont_look_inside`, the wrapper is a plain `if`, and
the two leaves get `jit_fnaddr` rows beside `has_hash_w_hook` /
`hash_w_hooked`.

Same value on every input: `eq_w_hooked` returns `false` when no hook is
installed and the wrapper only calls it under `has_eq_w_hook`, so `None` still
means "no hook" and `Some(false)` still means "not equal".

Census under `PYRE_DYN_INDIRECT=1`: `pyre_object::dict_eq_hook::EQ_W_HOOK`
19 -> 0.  Distinct roots and records do not move (49 / 198 before and after)
because the same nineteen graph heads — set-identical, not merely equal in
count — re-root at `map::IndexMap::get`, which sat directly behind it.  Their
cascade depth collapses 3 -> 1 (`dict_keys_equal -> try_eq_w -> EQ_W_HOOK`
becomes a direct failure), so depth-1 goes 66 -> 85 and depth-3 52 -> 33.
Gate off is unchanged at 45 / 214.

This changes generated code — one blocked graph becomes two residual calls —
so it was validated end to end rather than by census alone.
`python3 pyre/check.py --backend dynasm,cranelift,wasm` — dynasm 328/328,
cranelift 328/328, wasm 325/325.

Assisted-by: Claude

* pyre: give IdentityDictStrategy::getitem a named residual lookup leaf

Of the seven `DictStrategy::getitem` impls, six delegate their storage probe
to a named `w_dict_lookup_*_strategy` / `w_module_dict_lookup_inner` helper.
`IdentityDictStrategy::getitem` was the exception: it called
`identity_storage(w_dict).get(&IdentityKey(w_key))` inline in the trait
method, so the raw `IndexMap::get` sat in the traced body with nowhere to
attach a marker.

Extract it as `w_dict_lookup_identity_strategy`, marked `dont_look_inside`
with the same citation the int twin already carries
(`rordereddict.ll_dict_getitem`, traced only for a virtual dict), and give it
a `jit_fnaddr` row beside `w_dict_lookup_int_strategy`.  `IdentityKey` hashes
and compares by address, so the residual boundary encloses no user Python code
at all — the discriminator that separates a residualizable per-key probe from
a container walk that has to stay traced.

Census under `PYRE_DYN_INDIRECT=1`: `map::IndexMap::get` 19 -> 15 and the
`getitem` graph clears entirely; the four freed graphs re-root at
`IndexMap::with_capacity` in `switch_to_object_strategy`, and the other
fifteen turn out to be a different graph (`w_module_dict_lookup_inner`).
Roots and records do not move yet (49 / 198).  Gate off unchanged at 45 / 214.

Assisted-by: Claude

* pyre: residualize the module-dict entries probe, not its router

`w_module_dict_lookup_inner` is `celldict.py:131-141`: a str fast path, a
never-equal-to-string shortcut, a strategy promotion, and — in two of its arms
— an `IndexMap::get` on the promoted object storage.  Only the probe is opaque
to the tracer; `celldict.py` carries no residual marker on the router itself,
so marking the whole function would swallow branches PyPy traces.

Both arms now call `w_module_dict_lookup_object_entries`, which is the probe
alone under `dont_look_inside` with the `rordereddict.ll_dict_getitem`
citation the other dict lookup leaves carry.  It returns `None` when the dict
is not on object storage, which is what the second call site's `?` spelled.

Also drops the `dont_look_inside` added to `w_dict_lookup_object_strategy`
while attributing this cluster: it measured bit-identical with and without,
and reverting it here does not bring the `getitem` graph back, so it was
carrying nothing.

Census under `PYRE_DYN_INDIRECT=1`: `map::IndexMap::get` 15 -> 0, distinct
roots 49 -> 48.  Records stay at 198 — the fifteen graphs re-root at
`core::slice::<Impl>::as_ptr`, which goes 11 -> 26.  Gate off unchanged at
45 / 214.

This and the preceding identity-leaf commit change generated code — a traced
map probe becomes a residual call — so they were validated end to end
together at this tree state rather than by census alone.
`python3 pyre/check.py --backend dynasm,cranelift,wasm` — dynasm 328/328,
cranelift 328/328, wasm 325/325.

Assisted-by: Claude

* pyre: spell the receiver-discounted argument count without saturating_sub

Nine sites across `type_methods` and `baseobjspace` computed "how many
arguments did the caller pass, not counting the receiver" as
`args.len().saturating_sub(1)` — always inside a TypeError message, always on a
gateway slice whose slot 0 is the bound receiver.  They now call a shared
`args_given`, which spells the same value as an explicit branch.

`core::num::<Impl>::saturating_sub` is an unregistered foreign leaf, so it
stopped every graph that reached one of these lines; a plain subtraction lowers
to `Sub` plus an overflow `Assert` the front end already strips
(`front/mir.rs:3326`).  Same value on every input, including the malformed
empty slice: `saturating_sub(1)` and the new branch both answer 0.

`resolve_dict_backing`'s `nslots.saturating_sub(newslotnames.len())` is a
different quantity, so it gets its own explicit branch rather than the helper.
The remaining `saturating_sub` callers (`call_valuestack`'s drop count, the
string-search cursors, the formatter width) are untouched — no census record
reaches them.

Census: `core::num::<Impl>::saturating_sub` 27 -> 0 under
`PYRE_DYN_INDIRECT=1` and 28 -> 0 with the gate off.  Records drop 198 -> 196
and 214 -> 212 — the first graphs to lift outright rather than re-root.  The
other twenty-two surface the next leaf in the same bodies
(`core::slice::<Impl>::get`, i.e. `args.get(2)` / `args.get(3)`), which is why
the distinct-root count reads 48 -> 49: one large root split into two smaller
ones.

`python3 pyre/check.py --backend dynasm,cranelift,wasm` — dynasm 328/328,
cranelift 328/328, wasm 325/325.

Assisted-by: Claude

* pyre: index the optional generator-throw arguments instead of slice::get

`generator_throw_impl`, its async twin, and the `send`/`asend` pair read their
optional positional arguments as `args.get(n).copied().unwrap_or_else(w_none)`.
They now call a shared `arg_or_none`, which spells the same value as an
explicit bounds test.

`core::slice::<Impl>::get` is an unregistered foreign leaf, and unlike the
integer helpers it cannot be folded either: it yields an `Option<&T>` whose
payload is valid on only one arm, and the front end's single
`Option`-synthesizing path writes the payload unconditionally in the call's
block, before the consumer's discriminant switch runs
(`emit_tagged_pair_aggregate`, documented at `front/mir.rs:9936`).  An
out-of-bounds read would therefore execute on the absent arm.  No lowering in
`front/mir.rs` splits a block, so there is no in-tree way to emit the guarded
read a fold would need.  A plain index has no such problem — it lowers to a
native read plus an overflow `Assert` the front end already strips.

Census: `core::slice::<Impl>::get` 27 -> 0 under `PYRE_DYN_INDIRECT=1` and
27 -> 0 with the gate off.  Roots and records are unchanged in both (62 / 245
and 59 / 249): all twenty-seven re-root at
`iter::adapters::skip::Skip::next`, which is a different function
(`dict_view_iter_reduce_method`), so the generator-throw family is now clear.

The same `args.get(n)` idiom appears at roughly 165 further sites across
`pyre-interpreter`.  They are left alone: no census record reaches them, and a
sweep of that size is worth doing on evidence rather than on symmetry.

`python3 pyre/check.py --backend dynasm,cranelift,wasm` — dynasm 331/331,
cranelift 331/331, wasm 327/327.

Assisted-by: Claude

* pyre: take the hash_str pointer/length inside the residual boundary

`try_hash_str` gates `hash_str_hooked(ptr, len)` on `has_hash_str_hook`, both
already `dont_look_inside`, but spelled `bytes.as_ptr()` in its own traced body.
`core::slice::<Impl>::as_ptr` is an unregistered foreign leaf — unlike
`slice::len`, which `is_container_len` folds (`front/mir.rs:8583`) — so the call
stopped `try_hash_str` and every caller above it.

`hash_str_hooked_bytes(&[u8]) -> i64` carries the marker and a `push_alias_pair`
row, so the pointer/length extraction runs inside a boundary that is already
opaque.  A `&[u8]` argument crosses a residual call unchanged
(`host_seam::emit_stdout` is the same shape) and the `i64` return is one word.

Census, re-extracted corpus, both gate states:

| root                               | OFF before | OFF after | ON before | ON after |
|------------------------------------|-----------:|----------:|----------:|---------:|
| `core::slice::<Impl>::as_ptr`      |          8 |     **0** |        23 |    **0** |
| `iter::adapters::skip::Skip::next` |         27 |     **0** |        27 |    **0** |
| records                            |        249 |       247 |       245 |      243 |
| distinct roots                     |         59 |        69 |        62 |       71 |

`try_hash_str` leaves the blocked-function list in both states.  The rising root
count is the analyzer descending further rather than a regression: with the
str-hash leaf lifted, the str-keyed dict path is walked for the first time and
reports the walls behind it — `dict_entries_get_str -> IndexMap::get` 18 and
`str_utf8_w -> Wtf8::as_str` 11 (gate ON), `w_dict_getitem_str`, `strategy_is`
and `w_dict_delitem` all at `__dyn_call` (gate OFF, 36 records the gate removes).
`Skip::next` reaching 0 is the same effect from the other side: the graphs that
used to reach `dict_view_iter_reduce_method` — `getattr_str_impl` and its
callers — now stop earlier, and `getattr_str_impl` reports as a root itself.

check.py green on all three backends.

Assisted-by: Claude

* pyre: residualize both entry-table probes and the _warnings state namespace

`dict_entries_get_str` spells `IndexMap::get` twice — once behind the borrowed
`StrLookupKey` when the str hash hook is installed, once behind
`object_key_for(w_str_new(key))` when it is not.  `dict_entries_probe_str` and
`dict_entries_probe_object` carry the marker and a `push_alias_pair` row each,
so both arms cross the same boundary `w_dict_lookup_int_strategy` and
`w_module_dict_lookup_object_entries` already sit behind — the oopspec'd
residual arm of `rordereddict.ll_dict_getitem`.  The hook gate, the stale-eq
reset and the arm selection stay traced.

Marking only one arm is inert: a body that reaches either `get` stops there, so
the enclosing graph reports the same wall.  Measured — with the borrowed probe
alone, `IndexMap::get` moved 19 -> 17; with both, 19 -> 4.

`_warnings::state_ns` reads a namespace pointer
`setup_after_space_initialization` stamps at runtime, so it is not a build-time
constant; it takes the `importing::sys_modules_dict` treatment (marked, bound by
qualified path, single-word `PyObjectRef` return) rather than an address row
that would fold whatever the build process saw.

Census, re-extracted corpus, both gate states:

| root                            | ON before | ON after | OFF before | OFF after |
|---------------------------------|----------:|---------:|-----------:|----------:|
| `map::IndexMap::get`            |        19 |    **4** |          4 |     **3** |
| `_warnings::STATE_NS`           |         6 |    **0** |          3 |     **0** |
| records                         |       243 |      236 |        247 |       244 |
| distinct roots                  |        71 |       70 |         69 |        68 |

`dict_entries_get_str` leaves the blocked-function list in both states.  Behind
it the next walls surface: `IndexMap::with_capacity` 5 -> 14 and `Wtf8::as_str`
19 -> 24 (gate ON).  The `with_capacity` records are almost all depth-1 — the
census enqueues the foreign method itself as a graph head under the
dyn-indirect widening — so they are out of reach of a pyre-side change.

check.py green on all three backends.

Assisted-by: Claude

* pyre: residualize the five typed-storage promotions

Every `switch_to_object_strategy` that rebuilds storage is `IndexMap`
construction and refill end to end — `with_capacity`, a loop of `insert`, a GC
box, then the strategy relabel.  The int, bytes, kwargs and identity strategies
each carried that body inline in their trait impl; the module dict carried it in
a free function.  Extracted the four impl bodies into free functions and marked
all five, with a `push_alias_pair` row each.  `mapdict_switch_to_object_strategy`
already had the marker and is unchanged.

Upstream carries no marker on
`dictmultiobject.py:1223 AbstractTypedStrategy.switch_to_object_strategy`, and
that is not an argument for tracing it here: upstream can trace the body because
every step is an RPython dict primitive the JIT models (`ll_newdict`,
`ll_dict_setitem`'s `look_inside_iff(isvirtual)`), while `IndexMap` has no
lowering at all, so the last modellable point is the call itself.  The `()`
return crosses the residual boundary the way `w_set_copy_storage_from` does.

Census, re-extracted corpus, both gate states:

| root                            | ON before | ON after | OFF before | OFF after |
|---------------------------------|----------:|---------:|-----------:|----------:|
| `map::IndexMap::with_capacity`  |        14 |    **0** |          1 |     **0** |
| records                         |       236 |      229 |        244 |       244 |

Seven of the fourteen advance to `Wtf8::as_str` (19 -> 30, gate ON) rather than
lifting outright; that wall is the `&str` fat return, which no marker can cross.

Marking only the two `dictmultiobject.rs` bodies first measured 14 -> 13: the
census names an inner graph by its LEAF name, and `switch_to_object_strategy`
has nine definitions across five files.  The remaining three live in
`kwargsdict.rs`, `identitydict.rs` and `mapdict.rs`.

check.py green on all three backends.

Assisted-by: Claude

* pyre: residualize the identity delete and index the extended-slice walks

`IdentityDictStrategy::delitem` spelled `IndexMap::shift_remove` in its own
body; `w_dict_delete_identity_strategy` takes it, leaving the keys-version bump
traced.  Upstream's `_ll_dict_del` (`rordereddict.py:884`) carries
`@jit.look_inside_iff(jit.isvirtual(d) and jit.isconstant(i))`, and an
`IndexMap` can never be virtual to this front end, so the predicate is
permanently false and the residual arm is the only reachable one.

`setitem_list_slice` and the bytearray extended-slice arm walked their index
list with `.iter().enumerate()`, and the bytearray arm wrote through
`slice::get_mut`.  Both are unregistered foreign leaves.  Rewritten as index
loops with an explicit bounds test — the shape
`listobject.py setitem__List_ANY_ANY` and
`bytearrayobject.py setitem__Bytearray_ANY_ANY` use for the same walk.

Census, re-extracted corpus, both gate states:

| root                                     | ON before | ON after | OFF before | OFF after |
|------------------------------------------|----------:|---------:|-----------:|----------:|
| `iter::adapters::enumerate::Enumerate::next` |     7 |    **0** |          5 |     **0** |
| `map::IndexMap::shift_remove`            |         5 |    **0** |          0 |         0 |
| records                                  |       229 |      229 |        244 |       244 |

The record totals do not move: every cleared graph re-blocks one step later.
Gate ON the successors are `wrapping_add` 1 -> 6, `from_raw_parts_mut` 1 -> 4,
`ctypes::borrow_memory` 3 -> 5 and `Map::collect` 9 -> 11; gate OFF,
`from_raw_parts_mut` 1 -> 4 and `__dyn_call` 44 -> 46.

The two new successors are named:

- `core::num::<Impl>::wrapping_add`, inner graphs `delitem` x5 and
  `w_dict_bump_keys_version` — the keys-version bump the delete leaf leaves
  traced.  `try_lower_wrapping_binop` (`front/mir.rs:9773`) already folds this
  call, but its width gate reads `tyref_literal_int_atom` and admits only
  `I64`/`Isize`, while `keys_version` is `usize` and serialises as
  `{"UInt": "Usize"}`, so the fold declines.
- `core::slice::raw::from_raw_parts_mut`, inner graph `object_items_slice_mut`
  — the `&mut [PyObjectRef]` view `w_list_setitem` goes through, now reached
  because the index walk no longer stops at `Enumerate::next`.

check.py green on all three backends.

Assisted-by: Claude

* majit(front): fold wrapping_{add,sub,mul} on the unsigned word bank too

`try_lower_wrapping_binop`'s width gate read `tyref_literal_int_atom` and
admitted only `I64`/`Isize`.  `usize` serialises as `{"UInt": "Usize"}`, which
that accessor does not see at all, so every unsigned wrapping op kept the
generic `Call` form and stopped its graph — the same `Int`-only-test defect
`vec_index_type_is_scalar`'s doc already records.  Gate on both banks the way
`try_lower_num_from`'s destination test (`front/mir.rs:10285-10291`) does.

Unsigned is the same machine op under a different rtyper dispatch:
`rint.py:217 rtype_add` -> `_rtype_template` prefixes `uint_`, and
`jtransform.py:1608-1610` renames `uint_{add,sub,mul}` straight back to
`int_{add,sub,mul}` for the JIT.  Still word-sized only — a narrower
`wrapping_add` wraps at its own width and keeps the `Call` form.

The emitted `BinOp` now carries the receiver's signedness instead of a flat
`ValueType::Int`.  `union_type` widens `Int u Unsigned` to `Unknown`
(`binaryop.py:191` UnionError), so annotating a `usize` sum as `Int` would
poison the merge where it meets the unsigned field read it came from.

Census, unchanged corpus (no re-extract — this is front-end only):

| root                             | ON before | ON after | OFF before | OFF after |
|----------------------------------|----------:|---------:|-----------:|----------:|
| `core::num::<Impl>::wrapping_add`|         6 |    **0** |          1 |     **0** |
| records                          |       229 |      228 |        244 |       243 |

The six were `w_dict_bump_keys_version` plus five `delitem` subjects reaching
it; `keys_version` and `clear_gen` are the `usize` counters involved.  Five of
them re-block on `IndexMap::shift_remove` in `w_module_dict_delitem_inner`, one
wall further down the same delete path.

check.py green on all three backends.

Assisted-by: Claude

* pyre: residualize the three leaves on the module-dict delete path

The five `delitem` subjects walk one chain, and clearing a link only exposes
the next, so all three are taken together.

`w_module_dict_delitem_inner` spelled `IndexMap::shift_remove` in both of its
arms (the object-strategy arm and the post-promotion arm).
`dict_entries_remove_object` takes the remove, the delete twin of
`dict_entries_probe_object` over the same `IndexMap<ObjectKey, PyObjectRef>`
table.  Upstream's `_ll_dict_del` (`rordereddict.py:884`) carries
`@jit.look_inside_iff(jit.isvirtual(d) and jit.isconstant(i))`; an `IndexMap`
can never be virtual to this front end, so the predicate is permanently false
and the residual arm is the only reachable one.  Both arms go through the leaf
— leaving one traced keeps the graph blocked whatever the other does.

`VersionTag::fresh` bumped a fn-local `static NEXT: AtomicU64` inline.
`next_version_tag_serial` is the read seam, marked because the counter is a
runtime-mutable global: the value the build process observes is not a constant
and folding it would hand every dict the same tag.  Upstream allocates a fresh
`VersionTag()` object here, which rtypes to a `malloc` the JIT models; a static
atomic has no llop counterpart, so the call is the last modellable point.

`notify_version_watchers` ran the `Vec::retain` sweep over its weak watcher
flags inline.  `sweep_version_watchers` takes the non-empty half; the
`is_empty` early-out stays traced, so a mutation with no watchers still makes
no call.  This is the `version?` quasi-immutable invalidation walk, and
upstream runs all of it outside traced code — `QuasiImmut.invalidate`
(`metainterp/quasiimmut.py`) iterates `looptokens_wrefs` from the residual
`jit_force_quasi_immutable` path, never from a trace.

Census, re-extracted at each step, gate ON (gate OFF is unmoved at 243
throughout — every one of these is a dyn-indirect-only wall):

| step | root cleared                    | before | after | records |
|------|---------------------------------|-------:|------:|--------:|
| 1    | `map::IndexMap::shift_remove`   |      5 | **0** | 228     |
| 2    | `celldict::<Impl>::fresh::NEXT` |      5 | **0** | 227     |
| 3    | `vec::Vec::retain`              |      4 | **0** | 227     |

Each step's five (then four) graphs re-blocked one link further down the same
delete path, which is why the record totals barely move.  After step 3 the
four remaining ones land on `Wtf8::as_str` (30 -> 34) — the fat `&str` return
no residual ABI can carry, and the terminus of this chain.

check.py green on all three backends.

Assisted-by: Claude

* pyre: residualize the checked module-dict probe and store

`w_module_dict_lookup_inner_checked` and `w_module_dict_store_inner_checked`
spelled `IndexMap::{get,insert,pop}` inline.  Three leaves take them:
`dict_entries_probe_hashed`, `dict_entries_insert_hashed` and
`dict_entries_pop_last`.  `IndexMap` has no lowering, so the call is the last
modellable point — the same argument the sibling `dict_entries_*` leaves carry,
and upstream's `_ll_dict_setitem_lookup_done` (`rordereddict.py:674`) is
`@jit.look_inside_iff(jit.isvirtual(d) and jit.isconstant(key))`, neither
conjunct of which can hold for an `IndexMap` here.

The key crosses the boundary DECOMPOSED into `hash: i64` + `obj: PyObjectRef`
rather than as an `ObjectKey`: a struct by value is not a residual-ABI argument
shape, and the caller already holds both halves.  `object_key_for_checked`
stays traced, which is required, not incidental — it clears the hash-error flag
on its `Err` path (`take_hash_error`, `dictmultiobject.rs:321`), so a design
that let the leaf swallow the raise and had the caller re-read the flag
afterwards would silently lose the error.  The post-probe
`take_dict_key_error()` test and the `insert`/`pop` undo sequencing likewise
stay traced.

Census, re-extracted corpus:

| root                     | ON before | ON after | OFF before | OFF after |
|--------------------------|----------:|---------:|-----------:|----------:|
| `map::IndexMap::insert`  |         4 |    **0** |          2 |     **0** |
| `map::IndexMap::get`     |         4 |    **0** |          3 |     **1** |
| records                  |       227 |      224 |        243 |       243 |

This finishes the `IndexMap` axis of the `PYRE_DYN_INDIRECT` flip cost: the
flip now adds **no** `IndexMap` root at all, down from +14 when the axis was
first enumerated.  The remaining `get_index_of` 3 is equal under both gates
(its `Option<usize>` return is two words with no niche — a separate wall).
Flip ledger is now OFF 243 -> ON 224, net -19.

Three of the freed graphs re-block on `FnOnce::call_once` (5 -> 7) inside
`callback_free_dict_op`, and the rest on `Wtf8::as_str` (34 -> 37).

check.py green on all three backends.

Assisted-by: Claude

* majit(front): make dyn-indirect routing the default

`dyn_indirect_enabled()` flips from `PYRE_DYN_INDIRECT=1` opt-in to on by
default with `PYRE_DYN_INDIRECT=0` as the kill switch, the shape
`tuple_per_shape_enabled` and `option_residual_narrow_enabled` already use.

`__dyn_call` is not a lowering, it is a placeholder: an unregistered synthetic
path that stops whatever graph reaches it.  Routing an inline-Field `dyn Trait`
call through `CallTarget::Indirect` is what `jtransform.py`'s `indirect_call`
does, so the gated side was the faithful one.  What kept it gated was the cost
of the widening it forces — routing through `Indirect` makes the annotator
annotate every family member's body, and `DictStrategy` has ten impls whose
`getitem`/`getitem_str`/`delitem` bodies read `IndexMap`.

Census at the flip point, same corpus, no re-extract (front-end only):

| state                    | records | biggest wall        |
|--------------------------|--------:|---------------------|
| `PYRE_DYN_INDIRECT=0`    |     243 | `__dyn_call` 48     |
| default (this commit)    | **224** | `Wtf8::as_str` 37   |

The flip removes 48 `__dyn_call` records and adds 30 across 7 roots:
`Wtf8::as_str` +16, `ctypes::borrow_memory` +5, `Map::collect` +3,
`Wtf8Buf::push` +2, `FnOnce::call_once` +2, `AtomicBool::store` +1,
`set_async` +1.  When that added set was first enumerated it was 44 records
across 14 roots, `IndexMap` supplying 14 of them; the preceding slices took
`IndexMap` to zero and the rest to the list above.  `Wtf8::as_str` is now over
half of what is left and is a distinct epic — a fat `&str` return crosses no
residual ABI.

Verified as exactly the gate: the default census is wall-for-wall identical to
the old `PYRE_DYN_INDIRECT=1` run, and the `=0` census is wall-for-wall
identical to the old default.

The auto-population of `>=2`-impl trait families (`lib.rs`) shares the same
predicate and flips with it.  That is required, not incidental: minting
base/impl subclass classdefs perturbs `pyre_struct_root_names` ->
`ensure_session` inheritance-id numbering even off-path, so the kill switch
only restores byte-identity if it restores both halves together.

check.py green on all three backends.

Assisted-by: Claude

* pyre: spell the callable dispatch and the callback-free dict probe without closures

`dispatch_callable` took its two arms as `impl FnOnce`, and
`callback_free_dict_op` took the probe body the same way.  Each puts an
`FnOnce::call_once` in front of the work it wraps, and a closure has no
counterpart in the lifted model, so every graph that reached one stopped
there.

`dispatch_callable` becomes `classify_callable -> Result<CallableKind,
PyError>`: a fieldless two-arm answer that the one production caller,
`call_callable_with_mode`, matches on with both arms written out.
`callback_free_dict_op` becomes a macro, so the seven probe bodies expand
where they are read; each one's early `return` becomes the `if`/`else`
value it was standing for.  Upstream spells both out at the call site —
`ll_dict_lookup`'s fast pass before the paranoia restart
(`rordereddict.py:1057`) is not a callable it hands off.

Census: `FnOnce::call_once` 7 -> 0.

Assisted-by: Claude

* pyre, majit: clear eight census walls — the underflow report, the str view, and six leaves

Each of these stopped whole graphs at a call the rtyper has no lowering
for.  Measured on the same corpus, `PYRE_RTYPER_VERBOSE=1` phaseA:

| wall                          | recs |
|-------------------------------|-----:|
| `bytecode::CodeUnits::deref`  | 88 -> 0 |
| `Wtf8::as_str`                | 33 -> 0 |
| `iter::…::Map::collect`       |  8 -> 0 |
| `sync::atomic::AtomicBool::store` | 6 -> 0 |
| `Wtf8Buf::push`               |  4 -> 0 |
| `rustpython_host_env::ctypes::borrow_memory` | 4 -> 0 |
| `Wtf8Buf::push_wtf8`          |  3 -> 0 |
| `majit_ir::eval_breaker_word::set_async` | 2 -> 0 |

**`report_stack_underflow`** was reached from `PyFrame::pop` on the branch
it never takes, so its instruction-window formatting — a `Vec<String>`
built by walking the `CodeUnits` deref — was lowered into all 88 graphs
that pop a value.  It moves to module scope as a `#[dont_look_inside]`
`&PyFrame` reporter; the `()` result is what crosses the boundary and the
panic still diverges.

**`Wtf8::as_str`** returns `Result<&str, Utf8Error>`, and the two-word `Ok`
payload crosses no residual boundary, so neither marking nor the existing
`is_string_to_str_identity` fold (which needs a destination that strips to
`str`) could take it.  Split the answer instead: `w_str_first_surrogate`
scans once behind `#[dont_look_inside]` and returns the offending code
point index — or `-1` — in one machine word, `w_str_is_utf8` reads the
cached `is_ascii` counts first (`unicodeobject.py:1245`; one byte per code
point is UTF-8 by construction) and only then scans, and
`as_str_unchecked` names the view the caller has already earned.  That
last one is the same string-value identity `String::as_str` is, so the
fold's leaf set gains it.  `str_utf8_w` now reports its
`UnicodeEncodeError` offset from the same scan rather than a second
`code_points().position()` walk.

**The string builders** get named leaves, matching how upstream reaches
them: `w_str_concat` (`ll_strconcat`, `rstr.py:425-428`, already
`@jit.elidable @jit.oopspec('stroruni.concat')` upstream — not traced
through there either), `w_str_from_codepoint`
(`rutf8.unichr_as_utf8`, `rutf8.py:40`), and `w_str_slice_codepoints`
(`_unicode_sliced`, `unicodeobject.py:1373-1379`).  `_warnings::show_warning`
assembles its line the same way and then writes it to `sys.stderr`, so the
whole printer residualises.

**`(0..n).map(|i| shadow_stack_get(base + i)).collect()`** appears five
times across the reroot-after-`next` paths plus twice in
`temporarily_as_objects`; each becomes the index loop it already was.

**`w_type_set_uses_object_getattribute`** joins its already-residual
`_setattr` twin — the read side folds to the field it aliases, a store has
no such fold — through the same `bool`-normalising C-ABI bridge.  The
eval-breaker's async bit is armed and cleared through
`arm_async_eval_breaker` / `disarm_async_eval_breaker`, since an OS signal
handler writes the same word.  `cdata_bytes_object` boxes the ctypes view
inside the boundary, so a one-word `Option<PyObjectRef>` crosses instead of
the `&[u8]` its caller was going to wrap anyway.

phaseA 288 -> 196.  That is 92, not the 148 the cleared walls held: a graph
that no longer stops at one of these walks on to the next one, so part of
the count relocates rather than disappears (`core::mem::drop` 2 -> 14,
`from_raw_parts` 2 -> 9, `saturating_sub` 0 -> 6).

check.py green on dynasm, cranelift and wasm.

Assisted-by: Claude

* majit: register vtable_method_ptr in the production blackhole's insns map

Routing `dyn Trait` calls through `CallTarget::Indirect` became the default
earlier in this branch, so the codewriter now emits `vtable_method_ptr/rd>i`
into real jitcodes.  `build_inline_call_only_bh_builder` hand-curates its
registered set, and that byte was not in it, so the two guards over the
production blackhole failed:

- `production_bh_builder_covers_every_build_emitted_opname` — the byte is in
  `build_emitted_insns()` (pyre's `asm.insns`) with no dispatch entry.
- `production_bh_builder_overlay_only_gap_snapshot` — the opname left the
  never-emitted list, which is the drift that snapshot exists to report.

Register the key and drop it from the snapshot.  `setup_insns(asm.insns)`
(`blackhole.py:58-59`) resolves every opname the assembler emitted, so a byte
outside the curated set reaches the unwired-opcode placeholder rather than a
handler.

The handler it binds stays `handler_vtable_method_ptr_unimplemented`.  No
layer consumes this op: there is no backend lowering (`assembler.rs`, "backend
lowering of the actual vtable slot read is not yet implemented"), the walker
answers `DispatchError::UnsupportedOpname`, and `PyreVtableMethodDescr` holds
`(trait_root, method_name)` as strings with nothing that resolves them to an
address.  The walker's abort is what keeps the handler unreachable — a trace
meeting the op never compiles, so no resume lands past it.  What the entry
buys is that if one ever does, the panic names the missing op instead of
reporting an unwired byte.  Its doc comment said pyre's hot path does not emit
this pattern; that is no longer true and is rewritten.

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