Skip to content

jit: admit direct heap stores in abort-free FOR_ITER bodies (#57)#448

Merged
youknowone merged 2 commits into
mainfrom
fib_recursive
Jul 9, 2026
Merged

jit: admit direct heap stores in abort-free FOR_ITER bodies (#57)#448
youknowone merged 2 commits into
mainfrom
fib_recursive

Conversation

@youknowone

@youknowone youknowone commented Jul 9, 2026

Copy link
Copy Markdown
Owner

What

Widen the JIT FOR_ITER loop-body allow-list (lever A / #57) so that a
straight-line FOR_ITER frame whose body writes the heap is JIT-compiled
instead of running the whole frame in the interpreter. Admits the direct
heap-store opcodes STORE_SUBSCR / STORE_ATTR / STORE_NAME / STORE_GLOBAL
and the LOAD_NAME that reads module globals — but only in an abort-free
body (no call, branch, or nested loop).

unsupported_jit_shapefor_iter_bodies_all_jit_safe previously rejected any
FOR_ITER frame whose loop body contained a heap store, sending the entire
frame to the interpreter. That frame-level rejection — not call-inlining
coverage — is the dominant JIT perf gap: while loops hit ~10.7×, function-local
for loops ~14.75×, but any for loop that writes the heap (d[i]=, o.x=,
module global) got 0× (never JIT).

Why abort-free only

This gate is a conservative adaptation, not an upstream mechanism. The FBW
single-pass walker recovers from a mid-body walk abort by refusing to
re-deliver
the in-flight FOR_ITER item (Option-C deliver/refuse), which drops
the rest of that iteration. The register-machine metainterp instead resumes
exactly: it reads the precise (jitcode, pc) from the guard's resume data,
setpositions a blackhole interpreter there and runs the body to its end
(blackhole_from_resumedata, resume.py:1312), so it never drops or doubles.

An abort-free body cannot reach the deliver/refuse path at all: with no call,
branch, or nested loop there is no abort after the store, so the store commits
exactly once and its result coincides with the exact-resume result. That makes
abort-free stores the observable-deviation-free subset.

Admitting store-bearing bodies that can abort (calls, branches, method loads)
was explored and reverted: when a body effect commits and a later op aborts,
the refuse-drop skips the rest of that iteration — a partial-iteration wrong
answer (for x: d[k]=v; l.append(x)l short by the number of aborts), not a
clean iteration drop. Converging those bodies requires porting the exact-resume
path (#215 P2 / #73 / task#50); until then they stay interpreted and correct.

Validation

  • check.py dynasm 159/159 + cranelift 159/159 (2/2 backend runs).
  • foriter57 run_matrix (JIT vs interp oracle) MATRIX OK on both backends.
  • for_iter gate unit tests 9/9: straight-line STORE_SUBSCR/STORE_ATTR
    bodies admitted; append / store+branch / store+call bodies rejected.
  • Partial-iteration-drop repros (d[k]=v; l.append, s.add; l.append,
    h(x); l.append) now match the interpreter exactly under JIT.

Perf (all previously 0× / never-JIT)

shape speedup
module-global g = g + i ~4.75×
list buf[i] = i (subscript store) ~2.6×
dict d[k] = i ~1.4×

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Expanded JIT support for more for-loop bodies that are simple and branch-free.
    • Direct assignments inside loops, including item updates and attribute writes, are now more likely to run with JIT acceleration.
    • Module-global reads used in straight-line loop bodies are also supported.
  • Bug Fixes

    • Loops with branches or function calls remain correctly excluded from JIT safety, preserving expected behavior.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2ed3c018-0b53-442e-832a-7d291a2a9118

📥 Commits

Reviewing files that changed from the base of the PR and between 54a0022 and 2daab3d.

📒 Files selected for processing (1)
  • pyre/pyre-jit/src/eval.rs

Walkthrough

Extends the FOR_ITER JIT eligibility gate in eval.rs by adding an abort-free detection helper for loop bodies, then widens the allow-list to permit direct heap-store opcodes and LOAD_NAME reads for such bodies. Corresponding unit tests are updated to reflect the new permissive behavior and its boundaries.

Changes

FOR_ITER abort-free gating

Layer / File(s) Summary
Abort-free body detection
pyre/pyre-jit/src/eval.rs
Adds for_iter_body_is_abort_free, a bytecode-range scan that determines whether a FOR_ITER body avoids calls, nested-loop iterator setup, and conditional/forward branches, meaning it cannot reach the walk-abort path.
Widened JIT-safety gate and tests
pyre/pyre-jit/src/eval.rs
Reworks for_iter_bodies_all_jit_safe to permit STORE_SUBSCR, STORE_ATTR, STORE_NAME, STORE_GLOBAL, and LOAD_NAME when the body is abort-free, replacing the prior strict allow-list-only rule; updates tests to assert straight-line stores (buf[i] = i, o.x = i) are JIT-safe while branch/call variants are still rejected.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • youknowone/pyre#387: Both PRs modify the FOR_ITER JIT-eligibility/safety allow-list logic in eval.rs.
  • youknowone/pyre#421: Both PRs change FOR_ITER JIT-path handling around in-body call/branch hazards.
  • youknowone/pyre#436: Related FOR_ITER deep-kept resume gap fix touching similar eval.rs logic.

Poem

A rabbit hops through loops so tight,
Checking stores that pose no fright,
No calls, no branches, straight ahead—
"buf[i] = i" is safe, it's said!
Hop, JIT, hop, through fields so free,
Abort-free bodies, safe as can be 🐇

✨ 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 fib_recursive

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

Copy link
Copy Markdown

🤖 Codex parity review

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

Codex did not produce a report (exit 1). Last log lines:

--------
user
Assess by static analysis whether our changes in git diff upstream/main are equivalent
to the corresponding RPython/PyPy source code. The RPython and PyPy sources
are available locally (under `rpython/` and `pypy/` in this repository).

If anything was ported incorrectly, report every instance in detail. After
collecting all differences, organize the report into separate sections:

1. Cases where our patch regressed PyPy parity compared to main
2. Other mismatches introduced by our patch
3. Mismatches that already existed before this patch
4. Structural adaptations

Exceptions: some differences cannot be ported 1:1 because of Python 3.11 vs
3.14 differences, opcode mismatches caused by using a CPython-compatible
compiler, GIL/free-threading differences, and fundamental implementation-
language differences between RPython and Rust. Mark those separately under
"Structural adaptations".

---

Output format requirements (so the report can be parsed mechanically and
posted/triaged automatically). Use these four headings VERBATIM, in this
order, and nothing else at heading level 2:

## 1. Regressions to PyPy parity introduced by this patch
## 2. Other mismatches introduced by this patch
## 3. Pre-existing mismatches (already present before this patch)
## 4. Structural adaptations

Under each heading, list every finding as a bullet. For each finding cite the
concrete `our_file.rs:line ↔ rpython_or_pypy_file.py:line` pair and quote the
divergence concisely. If a section has no findings, still emit the heading
followed by a single line `None.` so all four sections are always present.
Do not modify any files; produce the report only.
2026-07-09T13:50:24.049842Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.078578Z ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when UnexpectedServerResponse("HTTP 401: {\n  \"error\": {\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\",\n    \"type\": null,\n    \"code\": \"token_expired\",\n    \"param\": null\n  },\n  \"status\": 401\n}")
2026-07-09T13:50:24.122970Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.165225Z ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when UnexpectedServerResponse("HTTP 401: {\n  \"error\": {\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\",\n    \"type\": null,\n    \"code\": \"token_expired\",\n    \"param\": null\n  },\n  \"status\": 401\n}")
2026-07-09T13:50:24.262151Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.320258Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.377155Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.528842Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
2026-07-09T13:50:24.613813Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.645379Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.669344Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.791271Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
2026-07-09T13:50:24.811040Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.904289Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:24.994431Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:25.013624Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:25.033532Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:25.135654Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
2026-07-09T13:50:25.169950Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:25.184983Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:25.200760Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-09T13:50:25.322458Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
ERROR: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.

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

ℹ️ 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/src/eval.rs Outdated
Comment on lines +3627 to +3630
| I::StoreSubscr
| I::StoreAttr { .. }
| I::StoreName { .. }
| I::StoreGlobal { .. }

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 Keep side-effecting FOR_ITER bodies out until resume is exact

Admitting these store opcodes makes the frame JIT-eligible even though the current recovery path explicitly refuses to deliver the consumed FOR_ITER item once a store has committed. If the trace walk aborts after the store on a later allowed opcode such as a CALL, LOAD_ATTR, or branch guard, fbw_foriter_inflight_take* returns None; the iterator has already advanced, so the rest of that iteration is skipped rather than resumed. That changes program results for common bodies like for x in xs: obj.x = x; total += g(x) where the store has run but the later local update/call is dropped; previously these frames stayed interpreted and preserved Python semantics.

Useful? React with 👍 / 👎.

@youknowone youknowone changed the title jit: widen FOR_ITER allow-list to admit heap stores and LOAD_ATTR (#57) jit: admit direct heap stores in abort-free FOR_ITER bodies (#57) Jul 9, 2026
@youknowone

Copy link
Copy Markdown
Owner Author

P1 confirmed — reverted. This is a real correctness regression, and the reproductions show it is not specific to the store opcodes:

body JIT interp / CPython
cnt[0]=cnt.get()+1; seen.append(x) (500, 498) (500, 500)
d[k]=v; seen.append(x) (8, 498) (8, 500)
s.add(x); seen.append(x) (500, 498) (500, 500)
h(x) (mutates a global); seen.append(x) (500, 498) (500, 500)

The FBW refuse-drop drops the rest of an iteration once any body effect has committed and a later op aborts — a partial-iteration wrong answer (seen short by the number of aborts), not the clean whole-iteration drop the gate comment claimed. The register-machine metainterp never does this: it resumes exactly at the guard's (jitcode, pc) (blackhole_from_resumedata, resume.py:1312) and runs the body to its end. So the deviation is the refuse-drop, not the allow-list widening.

Remediation: reverted the LOAD_ATTR admission and the unconditional-store admission, keeping only the abort-free store slice — an abort-free body (no call, branch, or nested loop) cannot reach the deliver/refuse path at all, so its store commits exactly once and coincides with the exact-resume result. The gate doc comment is rewritten to state this and to record that converging the callful/branchy/method-load bodies requires porting the exact-resume path (#215 P2 / #73 / task#50). check.py dynasm 159/159 + cranelift 159/159; the four repros above now match the interpreter under JIT.

commented 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: 82054dcb9d

ℹ️ 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/src/eval.rs
Comment on lines +3698 to +3702
|| (abort_free
&& matches!(
body_instr,
I::StoreSubscr
| I::StoreAttr { .. }

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 stores before abort-capable opcodes

The earlier comment named CALL/LOAD_ATTR/branch; in the current diff those are excluded, but BinaryOp and other base-listed guard/residual opcodes remain allowed after this store widening. For a syntactically call/branch-free body such as for x in data: buf[0] = x; total = total + x with an int-to-float transition like the existing foriter57/f1_polyonly pattern, this admits the frame: the store residual marks a body effect, then a later arithmetic guard abort makes Option-C refuse re-delivery, so the consumed item never reaches the remaining StoreFast and total silently drops that element. The abort-free test needs to reject stores before any opcode that can still abort, not just explicit calls and branches.

Useful? React with 👍 / 👎.

A FOR_ITER frame ran entirely in the interpreter when any loop body
contained STORE_SUBSCR/STORE_ATTR/STORE_NAME/STORE_GLOBAL, since those
opcodes were absent from the for_iter_body_op_is_jit_safe allow-list.

Add for_iter_body_is_abort_free() and permit those store opcodes plus
LOAD_NAME when the body contains no call, no nested FOR_ITER, and no
conditional/forward branch. Such a body has no reachable mid-loop walk
abort, so the eager store is committed exactly once and cannot be
dropped or doubled (#57). LOAD_ATTR stays excluded; the call-bearing
path is unchanged.

Assisted-by: Claude
Rewrite the `for_iter_bodies_all_jit_safe` doc comment to state that the
whole gate compensates for the FBW single-pass walker's Option-C
refuse-drop, which drops the rest of an iteration on a mid-body abort,
whereas the register-machine metainterp resumes exactly at the guard's
`(jitcode, pc)` (`blackhole_from_resumedata`, resume.py:1312) and never
drops. Records the convergence path (#215 P2 / #73 / task#50) and states
why abort-free bodies are the observable-deviation-free subset and why
LOAD_ATTR stays excluded.

Assisted-by: Claude
@youknowone youknowone merged commit b98b4cd into main Jul 9, 2026
@youknowone youknowone deleted the fib_recursive branch July 9, 2026 13:42

@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: 2daab3dde4

ℹ️ 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/src/eval.rs
Comment on lines +3703 to +3705
| I::StoreName { .. }
| I::StoreGlobal { .. }
| I::LoadName { .. }

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 NAME opcodes when locals differ from globals

This admits STORE_NAME/LOAD_NAME in any abort-free FOR_ITER body, including class bodies and exec(code, g, l) frames where w_locals != w_globals. The trace-side implementation does not preserve that namespace: MIFrame::store_name_value records the write to trace_globals_ptr() (pyre-jit-trace/src/opcode_handler_impls.rs:887-889), and load_name_value first tries a module-global cell path from concrete_namespace. A hot class loop such as class C: ...; for i in range(n): x = i can therefore start writing i/x to module globals, or read a global shadow, once this gate lets the loop JIT; before this change these FOR_ITER bodies stayed interpreted because the NAME opcodes were not allow-listed.

Useful? React with 👍 / 👎.

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