Skip to content

jit, interp: inline bound methods and defaults, specialize builtin calls, and fold the per-opcode polls into the eval breaker word - #878

Merged
youknowone merged 18 commits into
mainfrom
gc-decouple
Jul 30, 2026
Merged

jit, interp: inline bound methods and defaults, specialize builtin calls, and fold the per-opcode polls into the eval breaker word#878
youknowone merged 18 commits into
mainfrom
gc-decouple

Conversation

@youknowone

@youknowone youknowone commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Twelve commits on the interpreter and JIT call paths. PR #862 (the gc/_io
finalizer work this branch previously carried) is merged; everything below is
new on top of it.

The interpreter's CALL fast path did not know about Method

baseobjspace.py:1243-1266 takes the Function valuestack path for both plain
and method-form calls, and :1254-1259 unwraps _Method before that path
rather than treating it as a generic callable. pyre's CALL handler declined on
a non-null null_or_self and on a Method callable, so obj.method(...) and
every module alias built as random.gauss = _inst.gauss allocated an
Arguments vec per call instead of going through funccall_valuestack.

The handler now reuses the null/self stack slot for w_instance, continues
with w_function, and passes methodcall with the receiver counted as one
extra argument (callmethod.py:85-94) while the popped width stays the
physical [callable, null_or_self, args...]. That is what lets the walker see
an ordinary _flat_pycall for obj.method(...), as PyPy does.

Per-opcode polls

bytecode_trace entered apply_all_thread_hooks — and its mutex-bearing slow
arms — at every opcode. PyPy's common path is only its trace check and action
ticker; pyre's extra obligation is noticing process-wide
_settraceallthreads / _setprofileallthreads changes, which
all_thread_hooks_current now answers with two generation loads.

The last commit folds the remaining independent polls into the
already-established eval breaker word — PyPy's ActionFlag is one process
breaker — adding EB_FINALIZING (bit2, terminal) and EB_GC_INTERP (bit3,
process-stable), plus JIT_BREAKER_MASK. Both eval loops now pay one relaxed
load where they previously polled park_if_finalizing,
gc_interp::safepoint and gc_sync::safepoint_poll separately. It also
carries the translated builtin-call work: builtin_wrapper_indirect_graphs /
build_indirectcalltargets, the builtin_kwargs_marker_dict and
method_arity_failure / method_noarg_failure gateway seams, and
shadow_stack_copy_range.

The gc_interp::enabled() commit hoists gc_interp::enabled() out of the JIT loop's safepoint
poll. EB_GC_INTERP supersedes it; it is kept as its own commit so it can be
dropped independently.

JIT entry cost

unsupported_jit_shape — a pyre-only safety gate that walks a code object's
whole constant tree and bytecode — ran at every back-edge
(maybe_compile_and_run) and every Python call (try_function_entry_jit),
where RPython's can_enter_jit / maybe_compile_and_run are unconditional.
The classification is an immutable per-graph fact, so it now lives in
CallControl.graph_jit_shapes beside jitcodes, keyed the same way, and
eval_with_jit_inner is the one place that computes it.

jd1 (unpackiterable_driver) goes back behind PYRE_JD1=1. Unlike RPython,
pyre drives it through the same MetaInterp.tracing slot as the bytecode
portal, so while a residual next() runs an arbitrarily large generator body
the jd1 trace holds only the opaque call — and the shared flag suppresses every
jd0 merge point that body reaches. The second driver stays dormant until it has
RPython's independent recursive-portal behaviour.

Walker specializations

type(x) is space.type(w_obj) upstream — promote __class__, return
getclass — so it lowers directly instead of residualizing the type object's
full descr_call. Exact dict.get(identity_key) guards the new
W_DictObject.keys_version descriptor (pyre's explicit form of the live
strategy-iterator state dictmultiobject.py:807-845 carries implicitly: key
insertion/removal/strategy replacement bumps it, value replacement deliberately
does not) to pin the resolved entry index, then reads that entry's value live.
int(), math.frexp, math.ldexp and math.isqrt join math.sqrt.

The math identity probes were comparing against the wrong pointer:
py_checked_arity_fn! wraps each body in a non-capturing closure, so a
BuiltinCode stores the wrapper, never sqrt itself.
register_jit_builtin_wrappers records the pointers the module namespace
actually installed, and the probes compare those — a rebound math.frexp still
declines the specialization. ll_math_frexp's pair is emitted as two pure
calls because the IR has no multi-result call opcode; jit_math_ldexp_raw
returns signed infinity on overflow so the finite-result guard deoptimizes and
the ordinary builtin raises OverflowError.

Bound-method and default-argument inlining

try_walker_inline_user_call unwraps a _Method callable, guards the Method
class and its w_function, and passes the live w_self field as the
receiver — baking one anchor's receiver would collapse bound methods that
differ only in self. Function.defs_w (function.py:188-193,217-231) now
fills a missing positional tail behind a GuardValue on a live defs_w
descriptor, with each default read out of the pinned tuple's wrappeditems;
that descriptor is deliberately mutable rather than quasi-immutable, because
function_set_defaults does not yet call do_force_quasi_immutable and
marking it would leave compiled loops alive after f.__defaults__ = ....
Method-form keyword calls prepend the receiver before the kwnames
permutation.

The replay-safety scan carried its exact-numeric fact as two whole-call
booleans, which cannot describe a method-form call where self is nonnumeric
and a later argument is an exact int. It now takes per-parameter
ExactNumericArg provenance and tracks exact-numeric and exact-int separately
per register and per frame slot, so
residual_call_is_specialized_plain_numeric_binop judges the actual operands
of each binop rather than the call's arguments.

Build

[profile.release] gets lto = "thin" and codegen-units = 1 — the same
cross-crate optimization [profile.dist] already inherits — so
cargo run --release, which is the benchmark surface, is not measuring
parallel-CGU barriers through the interpreter's dispatch graph.

Corrections to the above

Three commits fix defects the work above introduced; each is separate so it can
be reviewed against the commit it corrects.

tyref_is_niche_option_ptr had been widened to fold Discriminant on
Option<&T> to a pointer null test. That is the Iterator::next result shape,
and front::iter_next recognizes the call by its __discriminant match
diamond — with the discriminant folded there is no diamond left, so the
residual Iterator::next() survived as exactly the unregistered callee the
rewrite exists to remove. The predicate now reads the kind field of the Charon
{"Ref": [region, ty, kind]} node and accepts mutable references only.

Two try_walker_inline_resolved_user_call declines are restored. A method-form
callee whose body method_form_callee_body_supported rejects is declined again
(the requires_seeded_callee_frame exemption is gone), and a Dirty
replay-safety body is not admitted by seeding its frame: its residual can
raise, and the local except that catches it is a callee-owned catch edge the
inline path does not compile, so the exception escaped the caller instead of
being handled where the source handles it. With Dirty rejected,
requires_seeded_callee_frame is always false and it and the three branches it
guarded are removed.

try_walker_inline_builtin_call propagated OrthodoxSubWalkTraceUnsupported
out of its sub-walk with ?. try_execute_residual_call_via_executor raises
that error before running the call, and this walk is the authoritative
executor, so the aborted trace resumed past a Python CALL whose effect never
happened — collections.deque.popleft() returned its value and left the
element in the deque, once per trace transition. The descent now cuts the trace
back to the position recorded before its first emitted op, resets the heap
cache and returns Ok(None) so the ordinary residual call runs, which is the
rollback orthodox_list_append_commit's callers already use. It is gated on
the descent having executed no journaled or unjournaled effect, since a descent
that applied one cannot be rewound this way and keeps the abort.

Gate

pyre/check.py --backend dynasm,cranelift,wasm on this branch matches
origin/main exactly: both are red only on synth/str_search_index_bounds
(compile.py:458 assert i == len(inputargs) failed (16 != 26), all three
backends). That failure was reproduced on origin/main alone in this worktree
with every branch-changed file reverted and the LLBC corpus re-extracted, and
independently in a second worktree; it is not from this branch.

authored by Claude

Summary by CodeRabbit

  • Performance

    • Improved JIT compilation and execution for built-in calls, method calls, indirect calls, and common array and slice operations.
    • Added faster specializations for type, dict.get, int, and several math functions, including frexp, ldexp, and isqrt.
    • Improved release-build optimization and numeric summation efficiency.
  • Bug Fixes

    • Improved handling of method arguments, keyword markers, null references, mutable-reference options, and array indexing.
    • Improved garbage-collection coordination, thread finalization, and safepoint behavior.
    • Corrected several JIT frame reconstruction and dispatch edge cases.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR updates interpreter safepoints, builtin dispatch, JIT specialization, indirect-call target generation, MIR lowering, frame reconstruction, and related build/runtime metadata across Pyre and Majit.

Changes

Runtime and JIT coordination

Layer / File(s) Summary
Eval-breaker and interpreter runtime
majit/majit-ir/..., pyre/pyre-interpreter/..., pyre/pyre-jit-trace/...
Adds finalization and interpreter-GC breaker bits, conditional safepoints, method-call handling, hook checks, and masked compiled-loop polling.
JIT shape caching and execution policy
pyre/pyre-jit/..., Cargo.toml
Caches unsupported-shape decisions, makes jd1 opt-in, caches bytecode-dump configuration, and updates release compilation settings.

Translation and dispatch

Layer / File(s) Summary
MIR lowering and graph operations
majit/majit-translate/src/front/..., majit/majit-translate/src/codewriter/..., majit/majit-translate/src/translator/...
Adds pointer-null, slice, array, mutable-reference, function-pointer, and ArrayLen lowering updates.
Indirect-call pipeline
majit/majit-translate/..., pyre/pyre-jit-trace/...
Discovers builtin wrapper graphs, records indirect-call target indices, serializes them, and reconstructs runtime targets.
Builtin gateways and specialization
pyre/..., pyre/pyre-macros/...
Centralizes keyword-marker detection, adds positional dispatch and wrapper registration, and specializes builtin, dictionary, math, and integer calls.

Inlining and frame state

Layer / File(s) Summary
FBW inlining and provenance
pyre/pyre-jit-trace/src/jitcode_dispatch/...
Adds bound-method/default handling, builtin-wrapper sub-walks, closure propagation, and separate numeric provenance tracking.
Frame reconstruction and root access
pyre/pyre-object/..., pyre/pyre-jit-trace/src/...
Adds shadow-stack range copying, free-variable initialization, and materialized inline-frame reconstruction from heap slots.

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

Possibly related issues

Possibly related PRs

Poem

A rabbit hops through breaker light,
Wrappers bloom in graphs tonight.
Arrays, frames, and math align,
Tiny targets trace a line.
JIT paths weave, the burrow sings—
Faster paws on clever springs.

🚥 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 concise and accurately captures the main changes: bound-method/default inlining, builtin-call specialization, and eval-breaker polling refactoring.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gc-decouple

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/700457b710bcbf5b7f0dd90f0128920b441947d4/pyre-jit-trace/src/jitcode_dispatch/specialize.rs#L5244-L5245
P2 Badge Guard bool isqrt operands as BOOL_TYPE

When the recorded argument is True or False, the admission check accepts it because pyre_object::is_int includes BOOL_TYPE, but this unboxes it behind an INT_TYPE class guard and then guards its w_class as canonical int. A bool cannot satisfy those guards, so a hot loop containing valid calls such as math.isqrt(True) produces an invalid or always-deopting trace instead of compiling. Use int_or_bool_unbox_type_descr for this path or explicitly decline bool arguments.

ℹ️ 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 29, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 42ab57c).
Updated: 2026-07-30T00:59:01.835Z

Files in the reviewed diff
Cargo.toml
majit/majit-gc/src/collector.rs
majit/majit-ir/src/eval_breaker_word.rs
majit/majit-metainterp/src/compile.rs
majit/majit-translate/src/codegen.rs
majit/majit-translate/src/codewriter/call.rs
majit/majit-translate/src/codewriter/jtransform.rs
majit/majit-translate/src/front/mir.rs
majit/majit-translate/src/lib.rs
majit/majit-translate/src/model.rs
majit/majit-translate/src/pipeline.rs
majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs
majit/majit-translate/src/translator/rtyper/rpbc.rs
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/call.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/executioncontext.rs
pyre/pyre-interpreter/src/gateway.rs
pyre/pyre-interpreter/src/jit_fnaddr.rs
pyre/pyre-interpreter/src/module/_random/mod.rs
pyre/pyre-interpreter/src/module/math/interp_math.rs
pyre/pyre-interpreter/src/module/math/mod.rs
pyre/pyre-interpreter/src/module/thread/mod.rs
pyre/pyre-jit-trace/build.rs
pyre/pyre-jit-trace/src/descr.rs
pyre/pyre-jit-trace/src/helpers.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.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/tests.rs
pyre/pyre-jit-trace/src/jitcode_runtime.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit-trace/src/trace_opcode.rs
pyre/pyre-jit-trace/src/unpack_state.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/jit/call.rs
pyre/pyre-macros/src/lib.rs
pyre/pyre-object/src/bufferview.rs
pyre/pyre-object/src/gc_interp.rs
pyre/pyre-object/src/gc_roots.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-jit/src/eval.rs:5107 ↔ rpython/jit/metainterp/warmspot.py:70 — jd1 is now disabled unless PYRE_JD1=1; PyPy configures every discovered JitDriver, so this regresses the secondary driver from default-on to opt-in.

2. Other mismatches introduced by this patch

None.

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

  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs:3325 ↔ rpython/jit/metainterp/resume.py:1049 — the retained InlineCallerFrameDecline::Unavailable => None fallback collapses an inlined callee onto caller-boundary resume. PyPy always decodes one frame per encoded (jitcode, pc) header. This fallback behavior was already present before the patch; the patch improves other paths but leaves it reachable.

4. Structural adaptations

  • majit/majit-ir/src/eval_breaker_word.rs:29 ↔ pypy/interpreter/executioncontext.py:464 — Pyre uses a process-global atomic bit word for async actions plus free-threaded STW/finalization/GC coordination; PyPy’s ActionFlag is a GIL-era ticker/action dispatcher. This is a free-threading adaptation.
  • pyre/pyre-interpreter/src/gateway.rs:542 ↔ rpython/rtyper/rpbc.py:216 — Rust function pointers erase RPython’s SomePBC target family, so generated gateway wrappers are recovered through a link-time registry and converted back to indirect-call candidates.
  • pyre/pyre-interpreter/src/eval.rs:4272 ↔ pypy/interpreter/baseobjspace.py:1255 — Pyre’s CPython-compatible CALL stack uses [callable, null_or_self, args…]; PyPy’s call_valuestack receives the callable separately. The bound-method unwrap preserves the corresponding PyPy fast-path semantics.

@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/216b2fa8d89356b908b231af33ccd4bf96367c93/pyre-jit-trace/src/jitcode_dispatch/specialize.rs#L4696-L4699
P1 Badge Validate the dict version inside the locked lookup

In the free-threaded runtime, another thread can insert or remove a key after the separate keys_version guard succeeds but before jit_dict_nth_value acquires the dictionary lock. Because these mutations can shift the IndexMap entry at index, the helper may return an unrelated key's value while the compiled dict.get operation should return the requested key's value or its default. The version check and indexed read need to occur atomically under the same dict lock, with a guard/side exit when the version no longer matches.

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

Here are some automated review suggestions for this pull request.

Reviewed commit: d6931323ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1920 to +1921
wrapper_items.extend_from_slice(&r_args[2..]);
wrapper_item_concretes.extend_from_slice(&arg_concretes[2..]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind CALL_KW arguments before entering builtin wrappers

For a hot keyword call to a generated builtin method, r_args[2] is the kwnames tuple and the remaining entries are the unbound positional/keyword values, but this copies all of them directly into the wrapper's positional argument array. For example, a method call such as rng.seed(w_n=5) enters the wrapper with [self, kwnames, 5] instead of [self, 5] (or the gateway's marker-dict representation), so the wrapper records an arity error for a call that the interpreter accepts. Reorder/bind CallKw operands before descending into the wrapper, or limit this specialization to CallFn.

AGENTS.md reference: AGENTS.md:L14-L20

Useful? React with 👍 / 👎.

Comment thread pyre/pyre-jit/src/eval.rs
Comment on lines +8082 to +8085
// The ordinary caller is `eval_with_jit_inner`, immediately after its
// one authoritative `unsupported_jit_shape` check. The other caller,
// `portal_runner_dispatch`, is recursive re-entry for a portal that could
// only have obtained compiled code after passing that same check. Keep

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retain the shape gate on recursive portal entry

When portal_runner_dispatch re-enters the portal for a callee reached from compiled/blackhole code, that callee has not necessarily passed eval_with_jit_inner's earlier shape check; this function can tick its entry counter and start tracing it even when it is CurrentFrameOnly, has a nested-break bridge-resume shape, or exceeds the constant encoding. Those are precisely the shapes unsupported_jit_shape prevents from being assembled or resumed, so such a recursive call can now enter an unsupported trace instead of remaining interpreted. Keep a cached shape check on this entry path rather than assuming every recursive frame was previously admitted.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (3)
pyre/pyre-macros/src/lib.rs (1)

1962-1974: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Route the variable-arity “too few” branch through a residual arity-helper.

Lines 1962-1973 still build the error with format!(...) inside every generated wrapper body, while the fixed-arity/no-arg branches use #[dont_look_inside]$crate::gateway::method_arity_failure / method_noarg_failure specifically to keep formatting out of traced code. Add a cold helper for the “expected at least N arguments” message and route this branch through it so no raw format! remains in the wrapper.

🤖 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-macros/src/lib.rs` around lines 1962 - 1974, Add a cold residual
helper for constructing the variable-arity “expected at least N arguments”
error, then update the visible variable-arity too-few branch in the generated
wrapper to call that helper instead of formatting inline. Match the existing
method_arity_failure and method_noarg_failure pattern, passing fn_name,
visible_required, and the adjusted argument count while preserving pluralization
and the current error message.

Source: Coding guidelines

majit/majit-translate/src/codewriter/call.rs (2)

3267-3277: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant builtin_wrapper_indirect_graphs() recomputation inside the BFS op loop.

builtin_wrapper_indirect_graphs() is already computed once at the top of find_all_graphs_bfs (line 3126); nothing mutates function_fnaddrs/function_graphs mid-walk, so recomputing the full HashMap scan + per-address sort here for every matching IndirectCall op is wasted work. See the consolidated comment for the cross-file fix.

🤖 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/codewriter/call.rs` around lines 3267 - 3277, In
find_all_graphs_bfs, reuse the builtin wrapper indirect-graphs map computed at
the function start instead of calling builtin_wrapper_indirect_graphs() inside
the IndirectCall branch for empty graphs. Keep cloning explicitly attached
graphs and skipping None unchanged, while preserving the existing empty-graphs
behavior through the cached result.

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

CallControl::builtin_wrapper_indirect_graphs() is recomputed redundantly across several call sites. It performs a full function_fnaddrs HashMap scan plus a per-address sort, and its result is stable once wrapper registration completes — yet it's invoked repeatedly with no caching.

  • majit/majit-translate/src/codewriter/call.rs#L3267-3277: hoist the call outside the BFS's per-op loop — it's already computed once at line 3126 with nothing mutating function_fnaddrs/function_graphs in between, so this inner-loop call is pure redundant work.
  • majit/majit-translate/src/codewriter/call.rs#L4538-4566: consider adding an internal cache (e.g. a RefCell<Option<Vec<CallPath>>> on CallControl, populated on first call) so every external caller benefits without each one needing to hoist manually.
  • majit/majit-translate/src/translator/rtyper/rpbc.rs#L326-345: lower_indirect_calls takes a single graph, suggesting the enclosing rtype driver invokes it once per candidate graph; verify that call frequency and, if it's per-graph, route through the same cache rather than recomputing per graph across the whole program.
🤖 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/codewriter/call.rs` at line 1, Cache the result of
CallControl::builtin_wrapper_indirect_graphs() inside CallControl so repeated
callers reuse one computed Vec<CallPath> after wrapper registration completes.
Update the method and its callers, including the BFS per-op path and
translator/rtyper lower_indirect_calls flow, to use the cache while preserving
the existing graph results.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@majit/majit-translate/src/codewriter/call.rs`:
- Around line 4538-4566: Make alias selection in builtin_wrapper_indirect_graphs
deterministic by adding a path-content tie-breaker after descending segment
count, such as lexicographic ordering of the path segments. Keep grouping by
function address and selecting the most-qualified alias unchanged, while
ensuring equal-length aliases always produce the same representative regardless
of function_fnaddrs iteration order.

In `@majit/majit-translate/src/front/mir.rs`:
- Around line 927-933: Update the impl-method branch in the graph identity
construction to set source_identity from owner and name only, using the
“{owner}::{name}” form without module_path. Leave the non-impl branch’s
fn_path-based identity unchanged.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs`:
- Around line 1729-1738: Update the eligibility guard in the inline-call
dispatch path to accept only PyreHelperKind::CallFn, excluding CallKw before
residual operands are passed to the builtin wrapper. Keep the existing
argument-count, destination-bank, and authoritative-executor checks unchanged.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs`:
- Around line 2896-2929: Extract the shared BINARY_OP tag decoding from the
neighboring helpers into a single helper such as body_binop_tag, and centralize
the And/Or/Xor/InplaceAnd/InplaceOr/InplaceXor check in one is_int_only_bitwise
predicate. Update both callers to reuse these symbols, and rename
residual_call_is_specialized_plain_int_binop to reflect that it only checks the
decoded operator, without claiming validation of d.key or the BinaryOp helper
kind.

---

Outside diff comments:
In `@majit/majit-translate/src/codewriter/call.rs`:
- Around line 3267-3277: In find_all_graphs_bfs, reuse the builtin wrapper
indirect-graphs map computed at the function start instead of calling
builtin_wrapper_indirect_graphs() inside the IndirectCall branch for empty
graphs. Keep cloning explicitly attached graphs and skipping None unchanged,
while preserving the existing empty-graphs behavior through the cached result.
- Line 1: Cache the result of CallControl::builtin_wrapper_indirect_graphs()
inside CallControl so repeated callers reuse one computed Vec<CallPath> after
wrapper registration completes. Update the method and its callers, including the
BFS per-op path and translator/rtyper lower_indirect_calls flow, to use the
cache while preserving the existing graph results.

In `@pyre/pyre-macros/src/lib.rs`:
- Around line 1962-1974: Add a cold residual helper for constructing the
variable-arity “expected at least N arguments” error, then update the visible
variable-arity too-few branch in the generated wrapper to call that helper
instead of formatting inline. Match the existing method_arity_failure and
method_noarg_failure pattern, passing fn_name, visible_required, and the
adjusted argument count while preserving pluralization and the current error
message.
🪄 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: 258d92aa-edc6-4519-9945-9ebcb720cd4e

📥 Commits

Reviewing files that changed from the base of the PR and between eced1b5 and 216b2fa.

📒 Files selected for processing (35)
  • Cargo.toml
  • majit/majit-gc/src/collector.rs
  • majit/majit-ir/src/eval_breaker_word.rs
  • majit/majit-translate/src/codegen.rs
  • majit/majit-translate/src/codewriter/call.rs
  • majit/majit-translate/src/codewriter/jtransform.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/src/lib.rs
  • majit/majit-translate/src/model.rs
  • majit/majit-translate/src/pipeline.rs
  • majit/majit-translate/src/translator/rtyper/rpbc.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/executioncontext.rs
  • pyre/pyre-interpreter/src/gateway.rs
  • pyre/pyre-interpreter/src/jit_fnaddr.rs
  • pyre/pyre-interpreter/src/module/math/interp_math.rs
  • pyre/pyre-interpreter/src/module/math/mod.rs
  • pyre/pyre-interpreter/src/module/thread/mod.rs
  • pyre/pyre-jit-trace/build.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/helpers.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.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_runtime.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/call.rs
  • pyre/pyre-macros/src/lib.rs
  • pyre/pyre-object/src/gc_interp.rs
  • pyre/pyre-object/src/gc_roots.rs

Comment on lines +4538 to +4566
/// Candidate PBC family for the generated `BuiltinCode.func`
/// function-pointer field.
///
/// RPython obtains this list from the annotator's `SomePBC`
/// descriptions. Pyre's generated wrappers publish real fnaddrs through
/// `jit_trace_fnaddrs`; pair those addresses with their registered source
/// graphs here. Aliases sharing an address name the same wrapper; select
/// the most-qualified source identity for the one graph object entered
/// into the PBC family.
pub fn builtin_wrapper_indirect_graphs(&self) -> Vec<CallPath> {
let mut by_address: std::collections::BTreeMap<i64, Vec<CallPath>> =
std::collections::BTreeMap::new();
for (path, &fnaddr) in &self.function_fnaddrs {
let Some(leaf) = path.last_segment() else {
continue;
};
if !leaf.starts_with("__pyre_wrap_") || !self.function_graphs.contains_key(path) {
continue;
}
by_address.entry(fnaddr).or_default().push(path.clone());
}
let mut result = Vec::new();
for mut aliases in by_address.into_values() {
aliases.sort_by_key(|path| std::cmp::Reverse(path.segments.len()));
result.push(aliases.remove(0));
}
result
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Non-deterministic tie-break when multiple wrapper aliases share the same address and segment count.

aliases is built by iterating self.function_fnaddrs (std::collections::HashMap), whose iteration order varies across process runs. sort_by_key(|path| Reverse(path.segments.len())) is stable, so when two aliases of the same address tie on segment length, the chosen "most-qualified" representative depends on HashMap iteration order rather than the path content — a reproducibility gap for something the doc comment describes as a deterministic "most-qualified" selection.

🔧 Proposed fix: deterministic secondary sort key
-            aliases.sort_by_key(|path| std::cmp::Reverse(path.segments.len()));
+            aliases.sort_by(|a, b| {
+                b.segments.len().cmp(&a.segments.len()).then_with(|| a.segments.cmp(&b.segments))
+            });
📝 Committable suggestion

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

Suggested change
/// Candidate PBC family for the generated `BuiltinCode.func`
/// function-pointer field.
///
/// RPython obtains this list from the annotator's `SomePBC`
/// descriptions. Pyre's generated wrappers publish real fnaddrs through
/// `jit_trace_fnaddrs`; pair those addresses with their registered source
/// graphs here. Aliases sharing an address name the same wrapper; select
/// the most-qualified source identity for the one graph object entered
/// into the PBC family.
pub fn builtin_wrapper_indirect_graphs(&self) -> Vec<CallPath> {
let mut by_address: std::collections::BTreeMap<i64, Vec<CallPath>> =
std::collections::BTreeMap::new();
for (path, &fnaddr) in &self.function_fnaddrs {
let Some(leaf) = path.last_segment() else {
continue;
};
if !leaf.starts_with("__pyre_wrap_") || !self.function_graphs.contains_key(path) {
continue;
}
by_address.entry(fnaddr).or_default().push(path.clone());
}
let mut result = Vec::new();
for mut aliases in by_address.into_values() {
aliases.sort_by_key(|path| std::cmp::Reverse(path.segments.len()));
result.push(aliases.remove(0));
}
result
}
/// Candidate PBC family for the generated `BuiltinCode.func`
/// function-pointer field.
///
/// RPython obtains this list from the annotator's `SomePBC`
/// descriptions. Pyre's generated wrappers publish real fnaddrs through
/// `jit_trace_fnaddrs`; pair those addresses with their registered source
/// graphs here. Aliases sharing an address name the same wrapper; select
/// the most-qualified source identity for the one graph object entered
/// into the PBC family.
pub fn builtin_wrapper_indirect_graphs(&self) -> Vec<CallPath> {
let mut by_address: std::collections::BTreeMap<i64, Vec<CallPath>> =
std::collections::BTreeMap::new();
for (path, &fnaddr) in &self.function_fnaddrs {
let Some(leaf) = path.last_segment() else {
continue;
};
if !leaf.starts_with("__pyre_wrap_") || !self.function_graphs.contains_key(path) {
continue;
}
by_address.entry(fnaddr).or_default().push(path.clone());
}
let mut result = Vec::new();
for mut aliases in by_address.into_values() {
aliases.sort_by(|a, b| {
b.segments.len().cmp(&a.segments.len()).then_with(|| a.segments.cmp(&b.segments))
});
result.push(aliases.remove(0));
}
result
}
🤖 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/codewriter/call.rs` around lines 4538 - 4566, Make
alias selection in builtin_wrapper_indirect_graphs deterministic by adding a
path-content tie-breaker after descending segment count, such as lexicographic
ordering of the path segments. Keep grouping by function address and selecting
the most-qualified alias unchanged, while ensuring equal-length aliases always
produce the same representative regardless of function_fnaddrs iteration order.

Comment on lines +927 to +933
let graph = if let Some(owner) = &self_ty_root {
graph
.with_owner_root(owner.clone())
.with_source_identity(format!("{module_path}::{owner}::{name}"))
} else {
graph.with_source_identity(fn_path.clone())
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect impl_method_owner_for_fundecl's return shape and a sample name_path()
# rendering for an impl method, to confirm the module_path/owner overlap.
ast-grep run --pattern 'fn impl_method_owner_for_fundecl($$$) { $$$ }' --lang rust majit/majit-translate/src/front/mir.rs
rg -n -A5 'fn impl_method_owner_for_fundecl' majit/majit-translate/src/front/mir.rs

Repository: youknowone/pyre

Length of output: 454


🏁 Script executed:

#!/bin/bash
set -e
# Map and inspect the relevant source_identity construction, fn_path derivation,
# impl_method_owner_for_fundecl implementation, and nearby call_target_segments behavior.
wc -l majit/majit-translate/src/front/mir.rs
sed -n '860,945p' majit/majit-translate/src/front/mir.rs
printf '\n--- impl_method_owner_for_fundecl ---\n'
sed -n '12500,12575p' majit/majit-translate/src/front/mir.rs
printf '\n--- nearby references to impl_method_owner_for_fundecl ---\n'
rg -n 'impl_method_owner_for_fundecl|with_source_identity|source_identity|owner_root' majit/majit-translate/src/front/mir.rs

Repository: youknowone/pyre

Length of output: 13115


🏁 Script executed:

#!/bin/bash
set -e
# Inspect strip_crate_prefix and search the repository for sample impl-method name/path spellings.
rg -n 'fn strip_crate_prefix|strip_crate_prefix\(' majit/majit-translate/src/front/mir.rs majit -g '*/mir.rs' -g '!**/target/**'
printf '\n--- sample Charon/type path references around Self and impl methods ---\n'
rg -n -A2 -B2 'pyframe::<PyFrame>|pyre_interpreter::pyframe::<' majit -S
printf '\n--- call_target_segments implementation around impl_method_owner_for_fundecl users ---\n'
sed -n '8245,8275p' majit/majit-translate/src/front/mir.rs
echo '--- all format!("{module_path}::{owner}::{name}" occurrences in tracked files ---'
rg -n 'format!("{module_path}::{owner}::{name}"|with_source_identity\(' majit/majit-translate/src/front/mir.rs

Repository: youknowone/pyre

Length of output: 4977


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- strip_crate_prefix ---'
sed -n '15245,15275p' majit/majit-translate/src/front/mir.rs
printf '%s\n' '--- module/name derivation and source identity ---'
sed -n '865,932p' majit/majit-translate/src/front/mir.rs
printf '%s\n' '--- with_source_identity occurrences ---'
rg -n 'with_source_identity\(' majit/majit-translate/src/front/mir.rs
printf '%s\n' '--- exact module_path owner name source_identity occurrences ---'
rg -n --fixed-strings 'format!("{module_path}::{owner}::{name}"' majit/majit-translate/src/front/mir.rs
printf '%s\n' '--- behavioral simulation from source text ---'
python3 - <<'PY'
sample = "pyre_interpreter::pyframe::pyframe::<Impl>::pyframe::<Impl>::pop"
stripped = sample.removeprefix("pyre_").removeprefix("majit_").removeprefix("pyre_")
print(stripped)
parts = stripped.rsplit("::", 1)
module_path, name = (parts[0], parts[1]) if len(parts) == 2 else ("", stripped)
print("module_path=", module_path)
print("name=", name)
# If Charon name_path includes an Impl segment before the method, the owner qualified path will likewise include it,
# because it resolves the impl payload to the ADT's name_path, not to the function's rsplit parent.
owner = module_path
print("current=", f"{module_path}::{owner}::{name}")
print("suggested=", f"{owner}::{name}")
PY

Repository: youknowone/pyre

Length of output: 5814


Don’t prepend module_path for impl-method owner identities.

For impl methods, stripping the crate prefix from Charon paths can be a no-op when the path does not start with the current crate name, so module_path may still already contain the implementation module and owner before owner is resolved. Baking it again as "{module_path}::{owner}::{name}" produces a malformed source_identity; use "{owner}::{name}" for the impl-method branch.

🤖 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 927 - 933, Update the
impl-method branch in the graph identity construction to set source_identity
from owner and name only, using the “{owner}::{name}” form without module_path.
Leave the non-impl branch’s fn_path-based identity unchanged.

Comment thread pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
Comment on lines +2896 to +2929
pub(crate) fn residual_call_is_specialized_plain_int_binop(
body_code: &[u8],
d: &DecodedOp,
num_regs_i: usize,
constants_i: &[i64],
) -> bool {
let Some(&i_len) = body_code.get(d.pc + 2) else {
return false;
};
if i_len == 0 {
return false;
}
let Some(&tag_reg) = body_code.get(d.pc + 3) else {
return false;
};
let Some(&tag) = (tag_reg as usize)
.checked_sub(num_regs_i)
.and_then(|constant_index| constants_i.get(constant_index))
else {
return false;
};
use pyre_interpreter::bytecode::BinaryOperator;
matches!(
pyre_interpreter::runtime_ops::binary_op_from_tag(tag),
Some(
BinaryOperator::And
| BinaryOperator::Or
| BinaryOperator::Xor
| BinaryOperator::InplaceAnd
| BinaryOperator::InplaceOr
| BinaryOperator::InplaceXor
)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the shared BINARY_OP tag decode + int-only operator set.

Lines 2902-2928 duplicate the constants-window tag decode and the And/Or/Xor/Inplace* set already present at Lines 2862-2891. The two copies must stay in lockstep — adding a bitwise operator to one and not the other silently mis-gates replay safety. Also note this helper, unlike its neighbour, validates neither d.key nor the BinaryOp helper kind, so its name over-promises relative to what it checks.

♻️ Suggested shape
fn body_binop_tag(
    body_code: &[u8],
    d: &DecodedOp,
    num_regs_i: usize,
    constants_i: &[i64],
) -> Option<pyre_interpreter::bytecode::BinaryOperator> {
    let &i_len = body_code.get(d.pc + 2)?;
    if i_len == 0 {
        return None;
    }
    let &tag_reg = body_code.get(d.pc + 3)?;
    let &tag = (tag_reg as usize)
        .checked_sub(num_regs_i)
        .and_then(|i| constants_i.get(i))?;
    pyre_interpreter::runtime_ops::binary_op_from_tag(tag)
}

fn is_int_only_bitwise(op: pyre_interpreter::bytecode::BinaryOperator) -> bool { /* one set */ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs` around lines 2896
- 2929, Extract the shared BINARY_OP tag decoding from the neighboring helpers
into a single helper such as body_binop_tag, and centralize the
And/Or/Xor/InplaceAnd/InplaceOr/InplaceXor check in one is_int_only_bitwise
predicate. Update both callers to reuse these symbols, and rename
residual_call_is_specialized_plain_int_binop to reflect that it only checks the
decoded operator, without claiming validation of d.key or the BinaryOp helper
kind.

`cargo run --release` gets the same cross-crate optimization the `dist`
profile already inherits.

Assisted-by: Claude
`all_thread_hooks_current` compares the execution context's
`trace_all_generation` / `profile_all_generation` against the process
counters, and `bytecode_trace` calls the updater only when they differ.
`bytecode_trace` and `bytecode_only_trace` become `#[inline(always)]`.

Assisted-by: Claude
baseobjspace.py:1254-1259 unwraps `_Method` before the Function
valuestack path. The CALL handler now writes the receiver into the
`null_or_self` stack slot, continues with `w_function`, and passes
`methodcall` to `funccall_valuestack` with the receiver counted as one
extra argument (callmethod.py:85-94) while the popped width stays the
physical `[callable, null_or_self, args...]`.

Assisted-by: Claude
jd1 shares the `MetaInterp.tracing` slot with the bytecode portal, so
while a residual `next()` runs a generator body the jd1 trace holds only
the opaque call and every jd0 merge point that body reaches is
suppressed. Restore the opt-in gate; the master JIT off-switches still
apply.

Assisted-by: Claude
The flag is process-stable after its first env read, so hoist it out of
the per-bytecode safepoint poll instead of crossing the
`dont_look_inside` gate and reloading its atomic on every iteration.

Assisted-by: Claude
`CallControl.graph_jit_shapes` holds the `UnsupportedJitShape`
discriminant keyed by code-object address, alongside the graph-keyed
`jitcodes`. `eval_with_jit_inner` classifies through the cache, and
`maybe_compile_and_run` and `try_function_entry_jit` drop their repeated
whole-frame scans, which ran on every back-edge and every Python call.

Assisted-by: Claude
`register_jit_builtin_wrappers` records the `py_checked_arity_fn!`
wrapper pointer each BuiltinCode actually stores for `math.sqrt`,
`math.frexp` and `math.ldexp`; `is_math_sqrt_function` moves onto that
comparison and `is_math_frexp_function` / `is_math_ldexp_function` join
it. `jit_math_frexp_mantissa`, `jit_math_frexp_exponent` and
`jit_math_ldexp_raw` are the raw entry points behind the emitted pure
calls.

`try_walker_specialize_builtin_type` lowers `space.type(w_obj)`.
`try_walker_specialize_builtin_dict_get` guards the new
`W_DictObject.keys_version` descriptor and reads the resolved entry
value through `jit_dict_nth_value`. `try_walker_specialize_int_call`,
`try_walker_specialize_math_frexp` and `try_walker_specialize_math_ldexp`
join them in `dispatch_residual_call_iRd_kind`.

Assisted-by: Claude
`try_walker_inline_user_call` unwraps a `_Method` callable
(baseobjspace.py:1254-1259), guards the Method class and its
`w_function`, and passes the live `w_self` field as the receiver
argument instead of baking one anchor's receiver.
`try_walker_inline_resolved_user_call` fills a missing positional tail
from `Function.defs_w` (function.py:188-193,217-231) behind a
`GuardValue` on the new live `defs_w` descriptor, then reads each
default out of the pinned tuple's `wrappeditems`. Method-form keyword
calls prepend the receiver before the `kwnames` permutation.

The blanket `POP_JUMP_IF_{NOT_}NONE` callee rejection is removed;
`requires_seeded_callee_frame` declines instead when the callee frame is
not actually seeded.

`fbw_callee_body_replay_safety` takes per-parameter `ExactNumericArg`
provenance and tracks the exact-numeric and exact-int facts separately
per register and per frame slot, so
`residual_call_is_specialized_plain_numeric_binop` judges the actual
operands of each binop; `residual_call_is_specialized_plain_int_binop`
supplies the bitwise-result fact.

Assisted-by: Claude
`tyref_is_niche_option_ptr` folded `Discriminant` to a pointer null test for
`Option<&T>` as well as `Option<&mut T>` and `Option<NonNull<T>>`.
`Option<&T>` is the `Iterator::next` result shape, and `front::iter_next`
recognizes that call by its `__discriminant` match diamond; with the
discriminant folded there is no diamond left to rewrite, so the residual
`Iterator::next()` survives as the unregistered callee the rewrite removes.

`type_node_is_ref` becomes `type_node_is_mut_ref` and reads the kind field of
the Charon `{"Ref": [region, ty, kind]}` node instead of accepting any `Ref`.

Fixes tests/test_iter_next.rs branch_loop_sum_lifts_next_to_iter_next_op and
tests/test_desugar_mir.rs lowers_desugar_mix_with_aggregate_and_question_mark.

Assisted-by: Claude
`try_walker_inline_resolved_user_call` declines again on:

- a method-form callee whose body `method_form_callee_body_supported`
  rejects.  The `requires_seeded_callee_frame` exemption is removed
  (synth/float_subclass_binop_dispatch, synth/polymorphic_binary_receiver).

- a `Dirty` replay-safety body.  Its residual can raise, and the local
  `except` that catches it is a callee-owned catch edge the inline path does
  not compile, so the exception escapes the caller instead of being handled
  where the source handles it (synth/bool_dunder_error_no_leak,
  synth/pickle_ctor_args).

With `Dirty` rejected, `requires_seeded_callee_frame` is always false; it and
the three branches it guarded are removed.

Assisted-by: Claude
`try_walker_inline_builtin_call` propagated
`OrthodoxSubWalkTraceUnsupported` out of its sub-walk with `?`.
`try_execute_residual_call_via_executor` raises that error before running the
call, and this walk is the authoritative executor, so the aborted trace
resumed past a Python CALL whose effect never happened:
`collections.deque.popleft()` returned its value and left the element in the
deque (synth/gc_deque_backing_list grow_each_round 2966 -> 987234, one
`popleft` dropped per trace transition).

The descent now cuts the trace back to the position recorded before its first
emitted op, resets the heap cache and returns `Ok(None)` so the ordinary
residual call runs — the rollback `orthodox_list_append_commit`'s callers
already use.  It is gated on the descent having executed no journaled or
unjournaled effect, since a descent that applied one cannot be rewound this
way and keeps the abort.

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: 42ab57c7a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2340 to +2343
let positional_defaults = if callee_args.len() < nparams {
let Some(defaults) =
(unsafe { positional_defaults_for_inline(callable, callee_args.len(), nparams) })
else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject defaults when variadic slots remain unbound

For a hot call such as def f(a=1, *args): return args; f(), this new default fill makes callee_args.len() == nparams, so the call becomes eligible for inlining even though nparams excludes the *args local. The inline frame seeds only the defaulted a slot and never constructs the empty tuple that the interpreter binds to args, so the compiled body can read an uninitialized/null local instead of (). Require fbw_callee_scope_is_positional_only(w_code) on this path or explicitly initialize the extra argument slots.

AGENTS.md reference: AGENTS.md:L14-L18

Useful? React with 👍 / 👎.

Comment on lines +5289 to +5291
let value = unsafe {
if !pyre_object::is_exact_builtin_instance(arg_obj) || !pyre_object::is_int(arg_obj) {
return Ok(None);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard bool inputs with the bool layout in isqrt

When tracing a hot math.isqrt(True) or math.isqrt(False) call, is_int admits the boolean because it includes BOOL_TYPE, but the emitted specialization subsequently unboxes and guards the operand as INT_TYPE with the canonical int class. That contradicts the recording-time boolean layout, causing the trace to be rejected or to side-exit on every execution even though the interpreter accepts bools. Either exclude bool here or select the bool-specific type and field descriptor as the other numeric specializations do.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
pyre/pyre-jit-trace/src/descr.rs (1)

2761-2792: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Set the correct index_in_parent for the ExecutionContext fields

The closure’s index is the global descriptor handle, while index_in_parent is the positional field slot within ExecutionContext. Hardcoding both fields to 0 deviates from the static group layout; emit distinct positional indices, e.g. 0 for sys_exc_value and 1 for topframeref.

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

In `@pyre/pyre-jit-trace/src/descr.rs` around lines 2761 - 2792, Update the
`field` closure in `EC_DESCR_GROUP` so `index_in_parent` is supplied per field
rather than hardcoded to zero, assigning positional slots 0 and 1 to
`sys_exc_value` and `topframeref` respectively while preserving their global
`index` values.
♻️ Duplicate comments (1)
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs (1)

1960-1970: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

CallKw residual operands are still fed into the builtin wrapper unpermuted.

The eligibility guard admits PyreHelperKind::CallKw, but a call_kw residual's ref list is [callable, null_or_self, kwnames, args...]. wrapper_items (Line 2139) is built from &r_args[2..], so kwnames becomes wrapper positional 0 and the array length handed to the wrapper's arraylen check is one greater than the real argument count. Any dict.get(k, default=…)-shaped builtin call routed here enters the generated wrapper with a shifted argument slice.

Restrict this path to CallFn, or skip/permute kwnames before building wrapper_items.

🐛 Minimal fix: restrict to the positional helper
-        || !matches!(
-            pyre_helper,
-            majit_ir::PyreHelperKind::CallFn | majit_ir::PyreHelperKind::CallKw
-        )
+        || pyre_helper != majit_ir::PyreHelperKind::CallFn
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs` around lines 1960 -
1970, Restrict the eligibility guard in the inline-call dispatch path to
majit_ir::PyreHelperKind::CallFn only, excluding CallKw until its kwnames
operand is explicitly removed or permuted before wrapper_items is built.
Preserve the existing positional argument handling and guard behavior for
CallFn.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@majit/majit-translate/src/lib.rs`:
- Around line 1909-1926: Update the builtin-wrapper target collection around
builtin_wrapper_indirect_graphs so every path missing from
call_control.jitcodes() emits a diagnostic before being discarded, matching the
mismatch handling in register_trait_families. Preserve the existing conversion
and extension of valid JitCodeHandle targets, and apply the same diagnostic
behavior to the corresponding target-building block as well.

In `@majit/majit-translate/src/translator/rtyper/rpbc.rs`:
- Around line 326-345: Add a focused unit test for lower_indirect_calls that
constructs an IndirectCall with Some([]) and verifies its graphs are replaced by
builtin_wrapper_indirect_graphs(), while an IndirectCall with Some(non_empty)
retains its original graphs.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs`:
- Around line 1613-1627: Separate the stored bound-method state from method_form
in try_walker_inline_resolved_user_call: retain method_form for the LOAD_METHOD
split-receiver shape and use bound_method presence for the unwrapped Method
shape. In pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs lines
1613-1627, stop making the two predicates equivalent; update lines 2586-2596 so
foriter_dirty_bound can be set without requiring !method_form, and rewrite lines
3086-3097 to gate the PopJump scan using the corrected distinction rather than
the currently tautological (bound_method.is_none() || method_form) condition.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs`:
- Around line 5289-5294: Update the guard in the visible unsafe value-extraction
block to explicitly reject bool objects before calling is_int or
w_int_get_value. Mirror the bool exclusion used by
try_walker_call_assembler_self_recursive, while preserving the existing Ok(None)
behavior for rejected arguments and int handling for non-bool exact builtins.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs`:
- Around line 96-113: Update the JitCode lookup in
random_core_residuals_use_registered_genrand32_address to select the target by
name == "random" only, then assert separately that decoded_ops contains exactly
two residual_call_r_i/iRd>i operations. Correct the expect message from
"rrandom" to clearly identify Random::random.

---

Outside diff comments:
In `@pyre/pyre-jit-trace/src/descr.rs`:
- Around line 2761-2792: Update the `field` closure in `EC_DESCR_GROUP` so
`index_in_parent` is supplied per field rather than hardcoded to zero, assigning
positional slots 0 and 1 to `sys_exc_value` and `topframeref` respectively while
preserving their global `index` values.

---

Duplicate comments:
In `@pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs`:
- Around line 1960-1970: Restrict the eligibility guard in the inline-call
dispatch path to majit_ir::PyreHelperKind::CallFn only, excluding CallKw until
its kwnames operand is explicitly removed or permuted before wrapper_items is
built. Preserve the existing positional argument handling and guard behavior for
CallFn.
🪄 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: 588dcb24-b12b-4499-a89b-85a31cf2befb

📥 Commits

Reviewing files that changed from the base of the PR and between 216b2fa and 42ab57c.

📒 Files selected for processing (41)
  • Cargo.toml
  • majit/majit-gc/src/collector.rs
  • majit/majit-ir/src/eval_breaker_word.rs
  • majit/majit-metainterp/src/compile.rs
  • majit/majit-translate/src/codegen.rs
  • majit/majit-translate/src/codewriter/call.rs
  • majit/majit-translate/src/codewriter/jtransform.rs
  • majit/majit-translate/src/front/mir.rs
  • majit/majit-translate/src/lib.rs
  • majit/majit-translate/src/model.rs
  • majit/majit-translate/src/pipeline.rs
  • majit/majit-translate/src/translator/rtyper/flowspace_adapter.rs
  • majit/majit-translate/src/translator/rtyper/rpbc.rs
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/executioncontext.rs
  • pyre/pyre-interpreter/src/gateway.rs
  • pyre/pyre-interpreter/src/jit_fnaddr.rs
  • pyre/pyre-interpreter/src/module/_random/mod.rs
  • pyre/pyre-interpreter/src/module/math/interp_math.rs
  • pyre/pyre-interpreter/src/module/math/mod.rs
  • pyre/pyre-interpreter/src/module/thread/mod.rs
  • pyre/pyre-jit-trace/build.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/helpers.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.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/tests.rs
  • pyre/pyre-jit-trace/src/jitcode_runtime.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit-trace/src/unpack_state.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/jit/call.rs
  • pyre/pyre-macros/src/lib.rs
  • pyre/pyre-object/src/bufferview.rs
  • pyre/pyre-object/src/gc_interp.rs
  • pyre/pyre-object/src/gc_roots.rs

Comment on lines +1909 to +1926
// `BuiltinCode.func` is a SomePBC function-pointer field. The ordinary
// translated indirect-call op contributes this family through
// `IndirectCallTargets`; pyre's interpreter call boundary hides that op
// behind the runtime `call_fn` helper, so publish the annotator's same
// finite wrapper family on the shared Assembler explicitly. The handles
// are the exact `CallControl.jitcodes` objects materialized by
// `grab_initial_jitcodes`, preserving RPython object identity.
let builtin_wrapper_targets: Vec<jitcode::JitCodeHandle> = call_control
.builtin_wrapper_indirect_graphs()
.into_iter()
.filter_map(|path| call_control.jitcodes().get(&path).cloned())
.map(jitcode::JitCodeHandle::from)
.collect();
codewriter
.assembler
.indirectcalltargets
.extend(builtin_wrapper_targets);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Silent gaps between the compile-time wrapper family and the runtime indirect-call target list.

builtin_wrapper_indirect_graphs() paths that are missing from call_control.jitcodes() are dropped here with no diagnostic, unlike other registry-mismatch sites in this file (e.g. register_trait_families) which eprintln! on a mismatch. Since the very same family list is baked into every IndirectCall.graphs marker in rpbc.rs::lower_indirect_calls for compile-time analysis, a silent drop here can desynchronize the runtime-validated target set from what the compiler assumed was callable.

♻️ Suggested diagnostic on drop
     let builtin_wrapper_targets: Vec<jitcode::JitCodeHandle> = call_control
         .builtin_wrapper_indirect_graphs()
         .into_iter()
-        .filter_map(|path| call_control.jitcodes().get(&path).cloned())
+        .filter_map(|path| {
+            let found = call_control.jitcodes().get(&path).cloned();
+            if found.is_none() {
+                eprintln!(
+                    "make_jitcodes: builtin wrapper {path:?} was never drained into jitcodes; \
+                     omitted from indirectcalltargets"
+                );
+            }
+            found
+        })
         .map(jitcode::JitCodeHandle::from)
         .collect();

Also applies to: 1937-1944

🤖 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/lib.rs` around lines 1909 - 1926, Update the
builtin-wrapper target collection around builtin_wrapper_indirect_graphs so
every path missing from call_control.jitcodes() emits a diagnostic before being
discarded, matching the mismatch handling in register_trait_families. Preserve
the existing conversion and extension of valid JitCodeHandle targets, and apply
the same diagnostic behavior to the corresponding target-building block as well.

Comment on lines 326 to +345
pub fn lower_indirect_calls(graph: &mut JitFunctionGraph, call_control: &CallControl) {
// Generated gateway wrappers enter the MIR graph as a plain function-
// pointer `IndirectCall` with `Some([])` as a deferred PBC-family marker.
// At rtype time CallControl owns both the translated graphs and the
// linker-resolved wrapper addresses, so fill the same `c_graphs` list
// `FunctionReprBase.call()` appends in rpbc.py:216.
let builtin_wrappers = call_control.builtin_wrapper_indirect_graphs();
for block in &mut graph.blocks {
for op in &mut block.operations {
if let OpKind::IndirectCall {
graphs: Some(graphs),
..
} = &mut op.kind
&& graphs.is_empty()
{
*graphs = builtin_wrappers.clone();
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider a dedicated unit test for the deferred-marker fill.

The new pre-pass silently depends on the invariant that Some([]) is only ever the gateway-wrapper marker (never a legitimately-empty family, since those are normalized to None elsewhere). A small regression test asserting a Some([]) IndirectCall gets filled with builtin_wrapper_indirect_graphs() and a Some(non_empty) one is left untouched would protect this invariant from silent regressions.

🤖 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/translator/rtyper/rpbc.rs` around lines 326 - 345,
Add a focused unit test for lower_indirect_calls that constructs an IndirectCall
with Some([]) and verifies its graphs are replaced by
builtin_wrapper_indirect_graphs(), while an IndirectCall with Some(non_empty)
retains its original graphs.

Comment on lines +1613 to +1627
let bound_method = if !method_form && unsafe { pyre_object::is_method(callable) } {
let function = unsafe { pyre_object::w_method_get_func(callable) };
let receiver = unsafe { pyre_object::w_method_get_self(callable) };
if function.is_null() || receiver.is_null() {
return Ok(None);
}
method_form = true;
Some(BoundMethodInline {
method_op: r_args[0],
function,
receiver,
})
} else {
None
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

method_form is set on the same branch that builds BoundMethodInline, making both new bound-method predicates tautological. Because method_form = true is assigned inside the is_method(callable) arm, bound_method.is_some() always implies method_form, and every other caller of try_walker_inline_resolved_user_call passes bound_method: None. Both downstream predicates that try to separate "stored bound method" from "LOAD_METHOD split receiver" therefore evaluate to a constant.

  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs#L1613-L1627: carry the stored-bound-method distinction separately from method_form (e.g. keep method_form for the split-receiver shape and rely on bound_method alone for the unwrapped-Method shape).
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs#L2586-L2596: drop the !method_form term so foriter_dirty_bound can actually be set, or the Dirty admission and the Lines 2751-2753 gate remain dead code.
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs#L3086-L3097: rewrite the (bound_method.is_none() || method_form) gate against the corrected distinction; today it is always true and the PopJump scan runs unconditionally.
📍 Affects 1 file
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs#L1613-L1627 (this comment)
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs#L2586-L2596
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs#L3086-L3097
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs` around lines 1613 -
1627, Separate the stored bound-method state from method_form in
try_walker_inline_resolved_user_call: retain method_form for the LOAD_METHOD
split-receiver shape and use bound_method presence for the unwrapped Method
shape. In pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs lines
1613-1627, stop making the two predicates equivalent; update lines 2586-2596 so
foriter_dirty_bound can be set without requiring !method_form, and rewrite lines
3086-3097 to gate the PopJump scan using the corrected distinction rather than
the currently tautological (bound_method.is_none() || method_form) condition.

Comment on lines +5289 to +5294
let value = unsafe {
if !pyre_object::is_exact_builtin_instance(arg_obj) || !pyre_object::is_int(arg_obj) {
return Ok(None);
}
pyre_object::w_int_get_value(arg_obj)
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

is_int accepts bool, so math.isqrt(True) records an int-payload unbox against a bool object.

is_exact_builtin_instance + is_int admits W_BoolObject (the same hazard try_walker_call_assembler_self_recursive calls out at Lines 714-719 of inline_call.rs). The subsequent walker_unbox_int(..., INT_TYPE) and w_int_get_value(arg_obj) then read the bool through the int accessor/descr. The exact-w_class guard emitted afterwards makes the compiled trace deopt, but the recording-time concrete stamped onto raw_int is read through the wrong accessor.

Decline bool explicitly, as the CALL_ASSEMBLER arm does.

🐛 Proposed fix
     let value = unsafe {
-        if !pyre_object::is_exact_builtin_instance(arg_obj) || !pyre_object::is_int(arg_obj) {
+        if !pyre_object::is_exact_builtin_instance(arg_obj)
+            || !pyre_object::is_int(arg_obj)
+            || pyre_object::is_bool(arg_obj)
+        {
             return Ok(None);
         }
         pyre_object::w_int_get_value(arg_obj)
     };
📝 Committable suggestion

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

Suggested change
let value = unsafe {
if !pyre_object::is_exact_builtin_instance(arg_obj) || !pyre_object::is_int(arg_obj) {
return Ok(None);
}
pyre_object::w_int_get_value(arg_obj)
};
let value = unsafe {
if !pyre_object::is_exact_builtin_instance(arg_obj)
|| !pyre_object::is_int(arg_obj)
|| pyre_object::is_bool(arg_obj)
{
return Ok(None);
}
pyre_object::w_int_get_value(arg_obj)
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs` around lines 5289 -
5294, Update the guard in the visible unsafe value-extraction block to
explicitly reject bool objects before calling is_int or w_int_get_value. Mirror
the bool exclusion used by try_walker_call_assembler_self_recursive, while
preserving the existing Ok(None) behavior for rejected arguments and int
handling for non-bool exact builtins.

Comment on lines +96 to +113
#[test]
fn random_core_residuals_use_registered_genrand32_address() {
let expected = pyre_interpreter::jit_trace_fnaddrs()
.into_iter()
.find_map(|(path, address)| {
(path == "module::_random::Random::genrand32").then_some(address)
})
.expect("genrand32 runtime fnaddr");
let random = crate::jitcode_runtime::all_jitcodes()
.iter()
.find(|jitcode| {
jitcode.name == "random"
&& crate::jitcode_runtime::decoded_ops(&jitcode.code)
.filter(|op| op.key == "residual_call_r_i/iRd>i")
.count()
== 2
})
.expect("rrandom Random::random jitcode");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

This selector will panic opaquely if the lowering changes.

Identifying the target JitCode by name == "random" and exactly two residual_call_r_i/iRd>i ops folds the assertion's premise into the lookup: a lowering change that emits three residual calls turns the intended assertion failure into .expect("rrandom Random::random jitcode"). Prefer selecting on name alone and asserting the residual-call count separately (the panic message also reads rrandom).

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

In `@pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs` around lines 96 - 113,
Update the JitCode lookup in
random_core_residuals_use_registered_genrand32_address to select the target by
name == "random" only, then assert separately that decoded_ops contains exactly
two residual_call_r_i/iRd>i operations. Correct the expect message from
"rrandom" to clearly identify Random::random.

@youknowone
youknowone merged commit 8c87851 into main Jul 30, 2026
16 of 19 checks passed
@youknowone
youknowone deleted the gc-decouple branch July 30, 2026 04:16
youknowone added a commit that referenced this pull request Jul 31, 2026
* perf(jit): cache exact jitcode assembly declines

* jit: expose manual builtin gateways to translation

* jit: preserve runtime hints and float bitcasts

* jit: inline stored bound methods through dirty bodies

* jit: preserve Python frames through builtin gateways

* jit: lower rebuilt Result shells as allocations

* jit: preserve nested loop and field identity

* jit: restrict the builtin-wrapper fold to positional calls

`try_walker_inline_builtin_call` admitted both `PyreHelperKind::CallFn` and
`CallKw`, then built the generated wrapper's flat positional argument array
from `r_args[2..]`.  A `call_kw` residual carries its kwnames tuple at arg
index 2 — `Instruction::CallKw` emits `callable, null_or_self, kwnames,
arg0..argN-1` and `bh_call_kw_<n>` consumes that same order — so the tuple
became the wrapper's first positional value and the array length exceeded
the real argument count by one.

Keywords reach a generated wrapper as the trailing `__pyre_kw__` marker dict
that `split_builtin_kwargs` strips, and this fold builds no such dict.
Decline `CallKw` so those calls keep the ordinary residual, as
`CallFunctionEx` already does.

Assisted-by: Claude

* jit: require a positional-only callee scope for the walker inline lever

`try_walker_inline_resolved_user_call` admitted a callee on
`callee_args.len() == nparams` alone, where `nparams` is `co_argcount` as
returned by `resolve_inlinable_callee`.  `co_argcount` counts neither
`*args` nor `**kwargs` nor keyword-only parameters, while the inline frame
seeding stores only `param_boxes[0..nparams]` into a `NewArrayClear` array,
so a `*args` local read PY_NULL where `pack_varargs` binds `()`.  The
positional-defaults fill widened the set of calls that reach the arity test.

Consult `fbw_callee_scope_is_positional_only` beside the arity gate in the
shared resolved half, which covers every admission path, and drop the now
redundant check in the CALL_FUNCTION_EX branch.

Assisted-by: Claude

* jit: decline bool operands in the math.isqrt specialization

`pyre_object::is_int` accepts a bool, but the emitted specialization unboxes
through `INT_TYPE` and guards the canonical `int` `w_class`, neither of
which holds for a bool singleton.

Assisted-by: Claude

* jit: re-check the dict keys_version under the dict lock

The `dict.get` specialization guarded `W_DictObject.keys_version` with an
unlocked field read and then called `jit_dict_nth_value`, which took the
dict lock only for the indexed read.  A key-set mutation between the two
compacts the `IndexMap`, so the promoted index named a different key.

Replace the helper with `jit_dict_nth_value_versioned`, which holds one
reentrant `w_dict_lock` across the version re-check and the read and returns
PY_NULL on a mismatch, and emit a `GuardNonnull` on its result.

Assisted-by: Claude

* jit: return the accepted binop class from one decode

`residual_call_is_specialized_plain_numeric_binop` and
`residual_call_is_specialized_plain_int_binop` each decoded the body
`BINARY_OP` tag out of the constants window and each carried the
`And`/`Or`/`Xor` (+ in-place) operator set, which had to stay in lockstep.

Return `Option<SpecializedBinop>` from the first and delete the second.

Assisted-by: Claude

* jit(descr): rank ExecutionContext field descrs by byte offset

`EC_DESCR_GROUP` built both fields through one closure that hardcoded
`index_in_parent: 0`.  `make_simple_descr_group` copies that value verbatim
and binds a parent SizeDescr, and `OptHeap::field_slot_index` prefers
`index_in_parent` over `descr.index()` whenever a parent is bound, so
`sys_exc_value` and `topframeref` resolved to one `PtrInfo._fields` slot.

Sort the specs by offset and stamp `index_in_parent` from that position, and
resolve both accessors by offset rather than by declaration order.

Assisted-by: Claude

* majit: order the builtin-wrapper alias pick totally and memoise the family

`builtin_wrapper_indirect_graphs` bucketed aliases by iterating the
`function_fnaddrs` HashMap and picked with `sort_by_key(Reverse(segment
count))`, which is stable, so two aliases of one address with equal segment
counts resolved on iteration order.  Compare the segment sequences and
demote the `crate` placeholder so the order is total.

The family was also rebuilt per IndirectCall op and per drained graph.
Memoise it in a `OnceCell`; `function_fnaddrs` and `function_graphs` are
written only in the setup phase that precedes every reader.  `lib.rs`
indexes `jitcodes()` instead of `filter_map`, since `grab_initial_jitcodes`
has already inserted every path in the family.

Assisted-by: Claude

* jit: consult the cached frame-shape classification on the portal entry paths

`try_function_entry_jit` and `maybe_compile_and_run` stopped consulting
`unsupported_jit_shape` on the premise that `eval_with_jit_inner` classifies
every frame first.  `portal_runner_dispatch` reaches both without that:
`compile_tmp_callback` bakes `portal_runner_adr` as the whole callee body and
the `!is_resolved` CALL_ASSEMBLER force leg calls the same shim, so the
counter tick could start a trace for an excluded shape.

Consult `cached_unsupported_jit_shape`, a pointer-keyed lookup into
`CallControl.graph_jit_shapes`, rather than the whole-frame scan that
classification cache replaced.

Assisted-by: Claude

* interp: route the variable-arity argument-count error through a gateway helper

The generated wrapper's "expected at least N arguments" branch built its
message with `format!` inline in the traced body, while the fixed-arity and
no-arg branches call `#[dont_look_inside]` `method_arity_failure` /
`method_noarg_failure`.  Add `method_min_arity_failure` carrying the same
attribute, register its fnaddr aliases, and call it from the macro.

Assisted-by: Claude

* jit: preserve struct identity across field owner spellings

* jit(descr): compare the reconciled field description on a get_field_descr cache hit

`GcCache::get_field_descr` mints a descr with reconciled metadata, but its
cache-hit `debug_assert!` compared the caller's raw arguments against it.

`derive_index_in_parent` re-derives the stored `index_in_parent` from the parent
that will actually be indexed, so a caller's own numbering never reaches the
cached descr. Pyre reaches one struct through several `all_fielddescrs` walks
that number their lists independently: the runtime group over the declared
payload numbers `W_IntObject.intval` 0, and the walk that models the inherited
`PyObject` header numbers it 2. `heaptracker.py:62-64` / `:102-103` skip
`typeptr` in both the list and the index, so the header-free numbering is the
upstream one. The assert reported every such split as a disagreement; it now
derives the caller's index the same way before comparing.

`front/mir.rs` leaves `SemanticProgram::immutable_fields` empty for the whole
LLBC pipeline — Charon serializes doc comments but not the
`#[jit_immutable_fields]` hint — so a spec built from that side reports
`(is_immutable, is_quasi_immutable) == (false, false)` for every field. The hit
path already resolves that by keeping the cached descr's flags; the assert now
compares those. A caller claiming purity a cached descr denies still trips.

Both fired only in debug builds, where they cost 28 `cargo test` failures: one
panic plus 27 tests failing on the descr mutex the panic poisoned. Downgrading
the assert to a print reports 16 distinct pairs over `W_IntObject`
`W_FloatObject` `W_LongObject` `Method` `W_Range` `W_IntRangeIterator`
`PyFrame` — 9 index-only, 7 immutability-only — and the suite passes with it
downgraded, so the cached descr already won.

The cache-hit message also names both descrs' owners, which is what identified
the two producers.

Assisted-by: Claude

* jit(bh): register new/d>r in the production blackhole builder

Lowering a rebuilt `Result` shell as an allocation made `OpKind::New`
reachable from the codewriter, so `build_emitted_insns()` now records
`new/d>r` while `build_inline_call_only_bh_builder`'s curated `setup_insns`
map did not carry the byte. `handler_new` was already wired, so this was a
registration gap, not an implementation gap: `wire_handler` no-ops without the
map entry and the byte stays unwired until a forward resume lands on it and
`dispatch_step` panics.

The operand shape matches the wired decoder. `assembler.rs OpKind::New` emits a
2-byte little-endian descr index then the 1-byte ref register holding the
result; `handler_new` reads exactly that through `read_descr` + `code[pos]`.
That is `new_with_vtable/d>r`'s shape, already registered beside it, and both
read `bh.cpu`, which this builder sets.

`production_bh_builder_covers_every_build_emitted_opname` and
`production_bh_builder_overlay_only_gap_snapshot` failed on this opname; the
snapshot drops it and records why it left, as it does for
`vtable_method_ptr/rd>i`.

cargo test --all --no-default-features --features dynasm: 0 failed.

Assisted-by: Claude

* test(jit): cover nested virtual append payloads

* test(jit): select the keyword wrapper's argument slice by descr, not by position

`keyword_builtin_wrapper_finds_colored_argument_slice_item_descr` picked the
wrapper's argument slice as "the first `arraylen_gc`" and pinned the entry
call's result colour to `inline_call_r_r/dR>r`. Both name a shape rather than
the property, and both stopped naming it once `split_builtin_kwargs` inlined
further: the entry call now yields the leading `args.is_empty()` test by value
(`inline_call_r_i/dR>i`) instead of the `(&[PyObjectRef], Option<PyObjectRef>)`
pair by reference, and the `args.len()` that inlined body reads off the
wrapper's own `r0` is now the first `arraylen_gc`.

The property held throughout. Every `getarrayitem_gc_r` carrying the
argument-slice item descr reads `r6`, and the only `arraylen_gc` on `r0` is that
pre-split `args.len()`. So the item descr is selected directly, the off-`r0`
assertion is made about the register that read reaches the slice through, and
the length read is required on that same register.

Reproducing this needs current LLBC: `build/llbc/` predating the lowering change
still yields the old shape, and the test passes against it on every target.
Verified with a fresh extraction — `pyre-jit-trace --lib`, 312 passed.

Assisted-by: Claude

* interp: drop the duplicate interp_return_log_enabled definition

`eval.rs` carries two definitions of `interp_return_log_enabled`, at :589 and
:617. Both are `#[cfg(not(feature = "sandbox"))]` with the same body — a
`OnceLock<bool>` over `PYRE_INTERP_RETURN_LOG` — and only their doc comments
differ, so `pyre-interpreter` fails to compile with E0428 and the Charon/LLBC
extraction step exits before any other CI job runs.

The pair is inherited, not produced by the rebase: `origin/main` `25b2442c4e`
holds both. #874 added the first; #907 added the second and merged on top
without seeing it, since each PR's CI builds only its own merge commit.

Keeps the :589 copy and its comment; the sole call site at :2729 is unchanged.

Assisted-by: Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant