Skip to content

jit: walker-native range FOR_ITER-next specialization with polymorphic demotion#545

Open
youknowone wants to merge 2 commits into
mainfrom
single-walker
Open

jit: walker-native range FOR_ITER-next specialization with polymorphic demotion#545
youknowone wants to merge 2 commits into
mainfrom
single-walker

Conversation

@youknowone

@youknowone youknowone commented Jul 14, 2026

Copy link
Copy Markdown
Owner

The single-walker line, containing a single commit on top of main.

jit: walker-native range FOR_ITER-next specialization with polymorphic demotion

  • Replaces the opaque jit_range_iter_next_or_null residual (EffectInfo::MOST_GENERAL, w_int_new-boxing every iteration) with inline getfield/IntAdd/SetfieldGc IR for range iterators, mirroring W_IntRangeIterator.next. The produced item stays boxed (Phase 1); the cursor advance is forward-delivered through fbw_foriter_inflight_capture, not journaled, so abort reconciliation is unchanged. Non-range iterators keep the residual.
  • Adds polymorphic demotion for the GuardClass(RANGE_ITER) at the FOR_ITER loop header: when the site is later driven by a non-range iterator the guard storms and the bridge cannot close. Since a monomorphic range loop never fails its own class guard, the first failure of that guard witnesses polymorphism. handle_fail invalidates the loop and declines the FOR_ITER green key (fbw_decline), retracing as the polymorphic-tolerant residual; the specialization declines a demoted site on retrace. Keyed on (green_key, fail_index) so an unrelated body guard in the same loop is not conflated.
  • Reported measurement: function-scoped for i in range(30_000_000): s += i drops from 0.47s to 0.14s.

Touches pyre/pyre-jit-trace/src/jitcode_dispatch.rs, pyre/pyre-jit-trace/src/trace.rs, and pyre/pyre-jit/src/eval.rs.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved JIT performance for loops iterating over integer ranges.
    • Added specialized handling that advances range iterators efficiently and preserves correct loop termination behavior.
  • Bug Fixes

    • Improved fallback behavior when specialized range iteration assumptions are no longer valid, helping prevent repeated use of unsuitable compiled loops.

@coderabbitai

coderabbitai Bot commented Jul 14, 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: 11 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

Run ID: 0e95315f-6bda-4257-8583-3b8f909276b9

📥 Commits

Reviewing files that changed from the base of the PR and between e3f69bf and 310fa58.

📒 Files selected for processing (7)
  • majit/majit-metainterp/src/jitdriver.rs
  • majit/majit-metainterp/src/optimizeopt/unroll.rs
  • majit/majit-metainterp/src/pyjitpl.rs
  • majit/majit-metainterp/src/warmstate.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit/src/eval.rs

Walkthrough

Adds walker-native specialization for W_IntRangeIterator FOR_ITER residuals, tracks specialized guard failures per thread, and invalidates affected JIT loops before resuming execution in the blackhole.

Changes

Range FOR_ITER specialization

Layer / File(s) Summary
Walker-native range iteration
pyre/pyre-jit-trace/src/jitcode_dispatch.rs
ForIterNext dispatch now specializes W_IntRangeIterator, updates iterator state, boxes yielded values, masks exhausted results to NULL, and derives the top-level loop green key.
Specialized guard failure demotion
pyre/pyre-jit-trace/src/trace.rs, pyre/pyre-jit/src/eval.rs
Thread-local metadata records specialized range FOR_ITER guard failures; matching failures decline and invalidate the loop before blackhole resumption.

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

Sequence Diagram(s)

sequenceDiagram
  participant FOR_ITER dispatch
  participant Walker specialization
  participant W_IntRangeIterator
  participant Trace failure tracking
  participant JIT driver
  FOR_ITER dispatch->>Walker specialization: specialize ForIterNext
  Walker specialization->>W_IntRangeIterator: update current and remaining
  W_IntRangeIterator-->>FOR_ITER dispatch: boxed item or NULL
  FOR_ITER dispatch->>Trace failure tracking: record specialized guard failure
  Trace failure tracking->>JIT driver: decline and invalidate loop
  JIT driver-->>FOR_ITER dispatch: resume in blackhole
Loading

Possibly related PRs

Suggested reviewers: lifthrasiir

Poem

I hopped through ranges, one step at a time,
With boxed-up carrots in orderly rhyme.
A guard missed its mark—down the loop went,
The blackhole caught what the walker sent.
“Fast paths!” I cheered, with a twitch of my nose.

🚥 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 accurately summarizes the main changes: walker-native range FOR_ITER-next specialization and polymorphic demotion.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch single-walker

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

❤️ Share

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

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 6b9a19b).

Files in the reviewed diff
majit/majit-metainterp/src/jitdriver.rs
majit/majit-metainterp/src/optimizeopt/unroll.rs
majit/majit-metainterp/src/pyjitpl.rs
majit/majit-metainterp/src/warmstate.rs
pyre/pyre-jit-trace/src/jitcode_dispatch.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-jit/src/eval.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-jit-trace/src/trace.rs:108 ↔ rpython/jit/metainterp/compile.py:701 — The new thread-local (green_key → fail_index) side table is not tied to a guard descriptor or trace identity. range_foriter_guard_failed can therefore classify a stale/replaced trace’s same-ordinal guard as the current range guard; upstream invokes handle_fail on the exact ResumeGuardDescr.

  • pyre/pyre-jit/src/eval.rs:5187 ↔ rpython/jit/metainterp/compile.py:701 — A matching range guard failure invalidates and removes the entire compiled loop, bypassing upstream’s start_compiling() / bridge-tracing path. PyPy resumes or compiles a bridge; it does not permanently demote a loop site after one class-guard failure.

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

  • majit/majit-metainterp/src/optimizeopt/unroll.rs:561 ↔ rpython/jit/metainterp/compile.py:384 — The retrace caller still uses the two-value wrapper, which discards quasi-immutable dependencies. Thus compile_retrace cannot union the saved preamble dependencies with peeled-body dependencies as upstream does before attaching the retraced loop.

  • majit/majit-metainterp/src/optimizeopt/unroll.rs:2028 ↔ rpython/jit/metainterp/compile.py:384ExportedState has no quasi_immutable_deps field. On the imported-state/retrace path, phase-1 dependencies cannot be retained and merged, whereas upstream retains them in start_state.

4. Structural adaptations

  • pyre/pyre-jit-trace/src/jitcode_dispatch.rs:19782 ↔ pypy/module/__builtin__/functional.py:732 — The walker explicitly lowers W_IntRangeIterator.next() into field reads/writes and a masked null result. This replaces source translation’s ordinary tracing of next() so the Rust full-body walker can retain heap-cache facts; for valid int-range iterators it preserves current, remaining, and returned-item semantics.

  • majit/majit-metainterp/src/optimizeopt/unroll.rs:1184 ↔ rpython/jit/metainterp/compile.py:313 — The dependency union is a deduplicated Vec rather than RPython’s dict-as-set. This is a Rust representation adaptation and preserves the required set-union behavior for ordinary two-phase compilation.

  • majit/majit-metainterp/src/warmstate.rs:568 ↔ rpython/jit/metainterp/warmstate.py:483 — Rust retains loop tokens with Arc and treats an invalidated token as absent; Python uses a weakref that may be absent. The new cleanup branch implements the same “seen token but no live procedure token” lifecycle.

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

ℹ️ 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/trace.rs Outdated
let specialized =
RANGE_FORITER_SPECIALIZED_GUARDS.with(|s| s.borrow().contains(&(green_key, fail_index)));
if specialized {
fbw_decline(green_key);

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 Keep demotion out of the global decline set

When this fires for a polymorphic FOR_ITER, it puts the green key into FBW_DECLINED_KEYS; however trace_bytecode checks that set before calling full_body_walk_trace and otherwise returns TraceAction::Abort through the retired trait path (trace.rs lines 621-651). That means a site that first traced as range and later sees a list/generator never gets the intended retrace with the generic ForIterNext residual after invalidation; every hot retry just skips the walker and aborts. This needs a separate range-demotion set that only makes try_walker_specialize_for_iter_next return None.

Useful? React with 👍 / 👎.

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

ℹ️ 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 +19832 to +19835
let guard_fail_index = ctx.trace_ctx.num_guards() as u32;
let type_const = ctx.trace_ctx.const_int(range_iter_type_addr);
ctx.trace_ctx
.record_guard(OpCode::GuardClass, &[iter_op, type_const], 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Key demotion with post-opt guard identity

This captures the range guard's ordinal from the recorder before optimizeopt runs, but handle_fail compares it with the backend fail_index assigned later by build_guard_metadata over the optimized compiled_ops. When any earlier guard is removed or folded (for example a heapcached class/null guard), the range guard's runtime index shifts, so a polymorphic list/seq iterator will miss the demotion path, or an unrelated guard at the old ordinal can be treated as the range guard. The demotion marker needs to be tied to a stable guard descriptor/trace identity or filled after guard indices are finalized.

Useful? React with 👍 / 👎.

…c demotion

Replace the opaque jit_range_iter_next_or_null residual (EffectInfo::MOST_GENERAL,
w_int_new-boxing every iteration) with inline getfield/IntAdd/SetfieldGc IR for
range iterators, mirroring W_IntRangeIterator.next. The produced item stays boxed
(Phase 1); the cursor advance is forward-delivered through fbw_foriter_inflight_capture,
not journaled, so abort reconciliation is unchanged. Non-range iterators keep the
residual.

A GuardClass(RANGE_ITER) at the FOR_ITER loop header storms when the site is later
driven by a non-range iterator: the guard fails on every re-entry and the bridge
cannot close. A monomorphic range loop never fails its own class guard, so the first
failure of that guard witnesses polymorphism. On that failure handle_fail invalidates
and removes the compiled loop and demotes the FOR_ITER green key. Demotion records the
key in a separate RANGE_FORITER_DEMOTED set rather than fbw_decline: the full-body walk
still runs on retrace and compiles the generic polymorphic-tolerant residual — only
try_walker_specialize_for_iter_next declines the demoted site — so a demoted polymorphic
site compiles one generic loop instead of going interpret-only.

The range class guard's per-trace ordinal is tracked per green key in a
green_key -> fail_index map that a retrace overwrites in place, so a non-range body guard
that happens to share a stale ordinal cannot be conflated with the range guard and
spuriously demote a monomorphic range site.

Function-scoped `for i in range(30_000_000): s += i` drops from 0.47s to 0.14s.

Assisted-by: Claude
The single-walker compile path (compile_loop_body via UnrollOptimizer)
collected quasi-immutable field dependencies in its phase-1/phase-2
optimizers but never returned them, so last_quasi_immutable_deps stayed
empty and register_quasi_immutable_deps registered no module-dict version
watcher. A new module-global key insert then bumped the dict version
without flipping the compiled loop token's invalidation flag, so the loop
was neither re-enterable (is_compatible mismatch -> run-compiled-skip every
iteration) nor recompiled, and it interpreted under JIT tax.

UnrollOptimizer::optimize_trace_with_constants_and_inputs_vable_at now
returns the merged phase-1/phase-2 quasi_immutable_deps; compile_loop_body
stores them into last_quasi_immutable_deps (compile.py:234) before
register_quasi_immutable_deps runs. maybe_compile / maybe_compile_with_key
evict a cell whose token has been invalidated (has_seen_a_procedure_token
&& get_procedure_token() is None) via cleanup_chain, and a retrace no longer
reuses an invalidated loop's front_target_tokens, so the next entry
compiles a fresh loop against the current version (warmstate.py:483-500).

global_reassign 0.56s->0.03s, global_reassign_obj 0.53s->0.02s; the loop
recompiles once per version bump (loops_compiled=4). run-compiled-skip
storm 900000->0. check.py 181/181 on dynasm and cranelift.

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