Skip to content

jit: the inlined-callee frame image and the escape attribution behind VableEscapedDuringResidualCall - #1015

Merged
youknowone merged 10 commits into
mainfrom
single-walker
Aug 4, 2026
Merged

jit: the inlined-callee frame image and the escape attribution behind VableEscapedDuringResidualCall#1015
youknowone merged 10 commits into
mainfrom
single-walker

Conversation

@youknowone

@youknowone youknowone commented Aug 4, 2026

Copy link
Copy Markdown
Owner

What this is

Nine commits on the path to removing VableEscapedDuringResidualCall (the port of
pyjitpl.py:3389 vable_after_residual_callSwitchToBlackhole(ABORT_ESCAPE)), which is
the single largest abort class in the synthetic corpus: 138 of 219 loops_aborted,
against abort: vable escape: 0 and forcings: 0 on the same fixtures under real
pypy3 7.3.22.

Eight of the nine are behaviour-neutral at the default configuration — they build the
frame image that the abort's removal will expose, plus the counters that measure it. The
ninth is a new self-check guard. No committed .jitstats baseline moves.

The commits

# what it does
1 AGENTS.md: run the PyPy oracle (PYPYLOG=jit-summary:- pypy3 <fixture>) as the first step for a behavioural orthodoxy question
2 split sys._getframe into sys_getframe (the @unwrap_spec wrapper) and getframe (the non-hidden chain walk), matching pypy/module/sys/vm.py:30 / :42, and add is_builtin_getframe_function
3 check.py self-check guard: an inlined callee reading its own frame image (_getframe(0)), the shape frame_lineno_mid_replay_regression does not cover
4 record the inlined callee's Python pc on its virtual frame at may-force boundaries — the coalesced form of pyopcode.py:200 dispatch_bytecode's per-bytecode last_instr store
5 report a virtualizable escape from the vable-token state alone, not from whether the image flush committed (virtualizable.py:231-246 tracing_after_residual_call)
6 attribute every escape force to the portal or the published-callee disjunct, and export both through MAJIT_STATS
7 write the folded inline-callee setfield_vable / setarrayitem_vable through to the live frame
8 materialize the folded inline-callee locals when a residual forces the frames
9 publish the callee's own pc when LiveLastInstrGuard retargets to its frame

What the counters now say

dynasm release, 356 synthetic fixtures:

loops_aborted                  207
attributed escape forces       138   (66.7% of aborts)
  by_portal                     93
  by_callee_only                45   (32.6% of the class, 21.7% of aborts)

Per fixture the attribution sums to that fixture's abort count exactly (r1 5+5=10,
r3 5+10=15) — the check that the counters name aborts rather than calls to the flush.

Why the abort itself is not removed here

Removing it is measured (207 → 128) but regresses one fixture, and this PR does not
carry a change that would need a .jitstats re-record to look green.

getframe_inline_subwalk_multiframe evaluates sys._getframe(2).f_locals["base"] + x.
Suppressing the published-callee disjunct takes it from loops_aborted=15 / loops_compiled=0 / guard_failures=0 to 1 / 2 / **8948** — the loop compiles and then
deopts on every iteration. The compiled loop holds exactly three CALL_MAY_FORCE, each
with its own GUARD_NOT_FORCED:

op source
v35 sys._getframe(2)
v42 .f_locals (jit_getattr)
v48 ["base"]

All 8947 deopts report fail=4 — the guard after v42, not after _getframe.
GUARD_NOT_FORCED is one word compare, cmp [jf_descr], 0, and the only writer of
jf_descr is Runner::force, so the guard cannot tell the portal force from the vref
force. Folding _getframe alone therefore just moves the deopt one op later:
.f_locals lowers to jit_getattr, which is #[jit_may_force], and its getset body
calls force_frame_before_locals_read unconditionally on the same portal frame.

PyPy has neither residual: pypy/module/sys/vm.py:41 carries
@jit.look_inside_iff(lambda space, depth: jit.isconstant(depth)), and its f_locals
read is ordinary looked-inside RPython whose virtualizable force
jtransform.py:2164-2172 deletes. The jit-log-opt oracle for this fixture shows
force_token() ×2, ptr_eq(p41, p0) + guard_true and no call at all.

So the remaining work is transparency, not gate-tuning: the constant-depth _getframe
arm and a non-forcing f_locals read. Tracked as follow-ups; this PR lands the frame
image they both depend on.

Verification

Rebased onto origin/main (9c3888c4), .ullbc re-extracted, all three backends measured
on the rebased tip:

  • check.py --backend dynasm 372/372, cranelift 372/372, wasm 368/368
  • cargo test --all --no-default-features --features dynasm — clean
  • cargo fmt --all -- --check — clean
  • committed .jitstats baselines unmoved; getframe_inline_subwalk_multiframe stays at
    loops_aborted=15 / loops_compiled=0 / guard_failures=0 / bridges_compiled=0
  • both bench/frame_*_regression.py self-check guards pass
  • r1r10 frame-read repros agree with PYRE_NO_JIT=1, CPython and pypy3, with two
    exceptions: r3 and r8 read sys._getframe(1).f_lineno from an inlined callee and
    report the caller's def line. Each commit here was measured to leave the r1r5
    answers unchanged, and no corpus fixture covers the shape. Filed as a separate
    follow-up; not addressed by this branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved sys._getframe() behavior, including frame-depth validation and reliable traversal.
    • Fixed frame information exposed by optimized and inlined calls, including line numbers, local variables, and instruction positions.
    • Improved handling of frame state during optimized calls and execution escapes.
  • Diagnostics

    • Added clearer JIT diagnostics for frame escapes and force decisions.
  • Tests

    • Added automated regression checks covering repeated frame inspection and multiple execution backends.
  • Documentation

    • Added guidance for comparing behavior with PyPy when investigating discrepancies.

@coderabbitai

coderabbitai Bot commented Aug 4, 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: 20 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6da3aa82-383c-41b7-820b-b946f7df01ac

📥 Commits

Reviewing files that changed from the base of the PR and between 0a8520d and 05df83c.

📒 Files selected for processing (13)
  • AGENTS.md
  • pyre/bench/frame_inlined_callee_own_image_regression.py
  • pyre/check.py
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/lib.rs
  • pyre/pyre-wasm-runner/src/main.rs
  • pyre/pyrex/src/lib.rs

Walkthrough

The PR adds sys._getframe handling, preserves inline-callee frame state across residual-call escapes, records escape classifications, exposes diagnostic counters, and adds a self-checking frame regression fixture.

Changes

Inline callee frame handling

Layer / File(s) Summary
Oracle guidance and frame regression validation
AGENTS.md, pyre/bench/frame_inlined_callee_own_image_regression.py, pyre/check.py
Documents PyPy oracle comparisons and adds an automated frame-image regression check.
Frame access and shadow state
pyre/pyre-interpreter/src/module/sys/vm.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/{inline_call.rs,mod.rs,vable_ops.rs}
Implements validated sys._getframe access and tracks concrete inline-callee frames, PCs, and folded values.
Residual-call frame publication and escape restoration
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
Publishes callee-specific frame PCs, restores folded state after escapes, and classifies portal and published-callee escapes.
Escape diagnostics and reporting
pyre/pyre-jit-trace/src/trace.rs, pyre/pyre-jit/src/{eval.rs,lib.rs}, pyre/pyre-wasm-runner/src/main.rs, pyre/pyrex/src/lib.rs
Adds escape counters, exposes counter reads, updates ring offsets, and reports machine-readable JIT statistics.

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

Sequence Diagram(s)

sequenceDiagram
  participant ResidualCall
  participant InlineCalleeFrame
  participant VableState
  participant EscapeDiagnostics
  ResidualCall->>InlineCalleeFrame: resolve and publish callee Python PC
  ResidualCall->>VableState: prepare virtualizable and virtual-reference state
  VableState->>InlineCalleeFrame: restore frame slots after escape
  ResidualCall->>EscapeDiagnostics: classify portal and published-callee escapes
  EscapeDiagnostics->>InlineCalleeFrame: attribute the subsequent force
Loading

Possibly related PRs

Poem

A rabbit watched the frame escape,
Then checked each line and local place.
The callee’s path was written right,
Escape counts now shine in light.
“PASS!” the little rabbit sings.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's main changes: inlined-callee frame images and escape attribution for VableEscapedDuringResidualCall.
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 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.

@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: 0a8520d4f8

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

// forcing also resets the token before writing fields
// (`resume.py:1405-1408`). Therefore a matched guard reports the
// escape even when the flush declined.
return true;

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 Require a resumable image before forcing redirected escapes

When an inlined callee exposes its published frame but the full and locals-only flushes decline (for example, because a live virtualizable entry has no concrete value), this unconditional true now clears the portal token and raises VableEscapedDuringResidualCall without committing a resume PC. If build_multi_frame_miframe also cannot reconstruct the missing state, the epilogue falls back to replaying from the walk entry even though the residual call already ran, so a non-idempotent callee can apply its effects twice. Keep redirected callee escapes conditional on a committed/constructible forward-resume image rather than forcing first and hoping reconstruction succeeds.

AGENTS.md reference: AGENTS.md:L24-L30

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 05df83c).
Updated: 2026-08-04T04:25:28.010Z

Files in the reviewed diff
AGENTS.md
pyre/bench/frame_inlined_callee_own_image_regression.py
pyre/check.py
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-jit/src/lib.rs
pyre/pyre-wasm-runner/src/main.rs
pyre/pyrex/src/lib.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/module/sys/vm.rs:660 ↔ pypy/module/sys/vm.py:42 — PyPy attaches @jit.look_inside_iff(...isconstant(depth)) to the actual getframe operation. Pyre adds is_builtin_getframe_function, but it has no call site, so it cannot make _getframe transparent to the walker. The new matcher is dead code and the call remains residual.

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

  • pyre/pyre-interpreter/src/module/sys/vm.rs:602-604 ↔ pypy/module/sys/vm.py:44-45 — Pyre calls forcing gettopframe() before gettopframe_nohidden(), while PyPy only reads gettopframe_nohidden(). This forces the virtualizable on every _getframe call, where PyPy’s constant-depth traced path does not.

  • pyre/pyre-interpreter/src/module/sys/vm.rs:617-619 ↔ pypy/module/sys/vm.py:49-52 — Pyre marks and immediately forces the returned frame, but omits PyPy’s audit(space, "sys._getframe", [f]). Audit hooks therefore cannot observe or veto _getframe.

  • pyre/pyre-interpreter/src/module/sys/vm.rs:596-619 ↔ pypy/module/sys/vm.py:42-54 — PyPy’s look_inside_iff permits the constant-depth frame walk to be traced through; Pyre executes the same walk as a residual builtin. This produces virtualizable-force/abort behavior absent from PyPy.

4. Structural adaptations

  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs:3550-3595 ↔ pypy/interpreter/pyframe.py:654-759 — Pyre explicitly maps an inline callee JIT PC to its own Python PC and writes last_instr before a residual call. This compensates for the Rust walker dispatching bytecode without PyPy’s normal frame execution loop.

  • pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs:274-291,619-651 ↔ rpython/jit/codewriter/jtransform.py:650-656 — Pyre mirrors folded fresh-frame writes into the concrete PyFrame; upstream’s fresh_virtualizable hint simply removes the JIT-level operation. The concrete mirroring is needed because Pyre executes residuals against a live Rust frame.

  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs:3598-3670 ↔ rpython/jit/metainterp/virtualizable.py:101-113 — Pyre “disarms” a folded inline callee by materializing its shadow slots after escape. This is a Rust walker/resume-image adaptation; upstream retains per-frame MIFrame state directly.

  • pyre/pyre-jit-trace/src/trace.rs:5904-5911 ↔ rpython/jit/metainterp/jitprof.py:146-157 — Added escape-attribution counters are Pyre diagnostics, not an upstream semantic counterpart.

  • pyre/pyre-wasm-runner/src/main.rs:522 ↔ rpython/jit/metainterp/jitprof.py:146-157 — The wasm runner’s diagnostic slot offset tracks Pyre’s expanded counter layout; this is backend-reporting infrastructure, not interpreter behavior.

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

🤖 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 `@AGENTS.md`:
- Around line 204-206: Specify the shell language on the fenced code block
containing the PYPYLOG command by changing its opening fence to use the sh
identifier, while leaving the command and closing fence unchanged.

In `@pyre/pyre-wasm-runner/src/main.rs`:
- Line 546: Update the standard MAJIT_STATS emission path alongside RING_BASE so
it reads slots 6–10 and publishes the fbw_escape_split and fbw_force_attrib
counters consistently with native output. Require the diagnostic export used for
these counters and fail when it is unavailable; do not limit this handling to
the verbose PYRE_WASM_JIT_STATS reader.
🪄 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: 0cb53639-5320-4c73-a3cb-06f8c7d8a572

📥 Commits

Reviewing files that changed from the base of the PR and between fb00dfb and 0a8520d.

📒 Files selected for processing (13)
  • AGENTS.md
  • pyre/bench/frame_inlined_callee_own_image_regression.py
  • pyre/check.py
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-jit/src/lib.rs
  • pyre/pyre-wasm-runner/src/main.rs
  • pyre/pyrex/src/lib.rs

Comment thread AGENTS.md
Comment on lines +204 to +206
```
PYPYLOG=jit-summary:- pypy3 pyre/bench/synth/<fixture>.py
```

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 | 🟡 Minor | ⚡ Quick win

Specify the fenced code-block language.

Line 204 starts a shell command block without a language identifier. Add sh to satisfy markdownlint.

Proposed fix
-```
+```sh
 PYPYLOG=jit-summary:- pypy3 pyre/bench/synth/<fixture>.py
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 204-204: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@AGENTS.md` around lines 204 - 206, Specify the shell language on the fenced
code block containing the PYPYLOG command by changing its opening fence to use
the sh identifier, while leaving the command and closing fence unchanged.

Source: Linters/SAST tools

// `pyre_jit_trace::trace::fbw_diag`.
if let Ok(fbw) = instance.get_typed_func::<u32, u64>(&mut store, "pyre_fbw_diag") {
const RING_BASE: u32 = 6;
const RING_BASE: u32 = 11;

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

Publish the new FBW counters under Wasm MAJIT_STATS.

Line 546 only updates the verbose PYRE_WASM_JIT_STATS reader. The standard MAJIT_STATS block does not read slots 6-10, so Wasm output omits fbw_escape_split and fbw_force_attrib while native output publishes them. Emit the same records from the MAJIT_STATS path and fail if the required diagnostic export is unavailable.

🤖 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-wasm-runner/src/main.rs` at line 546, Update the standard
MAJIT_STATS emission path alongside RING_BASE so it reads slots 6–10 and
publishes the fbw_escape_split and fbw_force_attrib counters consistently with
native output. Require the diagnostic export used for these counters and fail
when it is unavailable; do not limit this handling to the verbose
PYRE_WASM_JIT_STATS reader.

`PYPYLOG=jit-summary:- pypy3 <fixture>` runs most `pyre/bench/synth` scripts
unmodified, so a behavioural "is this ours or upstream's?" question is one
command rather than a source-reading exercise. The section names the counters
to read, says a differing counter is a pointer to a JIT hint rather than the
answer itself, and records the case that prompted it: the
`getframe_inline_subwalk_multiframe` GUARD_NOT_FORCED count, held to be
unfixable by construction, against an oracle reporting `forcings: 0` /
`virtualizables forced: 0` and `pypy/module/sys/vm.py:41`'s
`@jit.look_inside_iff(lambda space, depth: jit.isconstant(depth))`.

The "When in doubt" grep under data-structure parity now points at it for
behavioural questions.

Assisted-by: Claude
`pypy/module/sys/vm.py` has two functions: `:30 _getframe`, carrying
`@unwrap_spec(depth=int)` and the negative-depth ValueError, over `:42
getframe`, carrying the non-hidden chain walk and
`@jit.look_inside_iff(lambda space, depth: jit.isconstant(depth))`. Pyre had
one anonymous closure holding both.

Split into `sys_getframe` and `getframe`, and add
`is_builtin_getframe_function` in the shape of
`builtins::is_builtin_len_function`. The registration was a closure, so no
identity predicate could name it; a named `fn` is the prerequisite for any
walker arm that wants to reproduce the walk rather than call it.

Behaviour is unchanged, deliberately: the walk keeps both virtualizable forces
(`gettopframe()` before it, `force_frame` after), which upstream does not take
because a constant depth is traced through instead. Their cost is now recorded
where they are: each call trips `vable_after_residual_call`, which is 138 of
the synth corpus's 219 `loops_aborted`, against `abort: vable escape: 0` and
`forcings: 0` on the same fixtures under real pypy3.

The doc comment also records why a constant-depth fold does not reclaim those
138 -- 70 are inside inlined callees whose virtual frame still carries
`last_instr = -1`, so folding would compile a `_getframe().f_lineno` that
reports the `def` line where the abort today falls back and answers correctly.

`cargo test --all --no-default-features --features dynasm` green;
`check.py --backend dynasm` 370/370; the getframe fixtures' abort counts and
`PYRE_NO_JIT=1` outputs are unchanged.

Assisted-by: Claude
`frame_lineno_mid_replay_regression` reads through `sys._getframe(1)` from a
callee whose caller is the virtualizable.  The new guard reads through
`sys._getframe(0)` from a callee that is itself inlined, so the frame handed out
is materialised through a `jit.virtual_ref` rather than being the
virtualizable: it carries the `last_instr = -1` sentinel from `helpers.rs` and
its locals region is not what the escape flush writes, that flush being keyed on
the virtualizable.  Three reads are pinned — `f_lineno`, `sorted(f_locals)`, and
the three combined.

Measured against a build with `sys._getframe`'s two `force_frame` calls moved to
the `f_lasti` / `f_lineno` getsets: the synthetic suite stayed at 369/370 on
dynasm and cranelift and 366/366 on wasm while `f_lineno` reported the `def`
line, `f_locals` grew a second entry, and the combined read exited 139.  This
guard exits 139 on that build and passes on dynasm, cranelift and wasm at HEAD,
as do cpython, pypy3 and `PYRE_NO_JIT=1`.

Assisted-by: Claude
…y-force boundaries

`maybe_walker_vable_and_vrefs_before_residual_call` now takes the current
jitcode pc and, when `inline_callee_consts` is present, records
`SetfieldGc(callee_frame, const(py_pc))` under `pyframe_next_instr_descr()`
plus `heapcache_setfield_cached`. The pc is mapped through the CALLEE's own
jitcode metadata (`consts.jitcode_index` -> `pyjitcode_for_jitcode_index` ->
`python_pc_for_jitcode_pc`); the sub-walk inherits `fbw_mode` from the outer
ctx, so `snapshot_sym` names the portal's jitcode and cannot map a callee pc.

Before this, the only `last_instr` store the trace ever recorded on such a
frame was the construction `-1` (`helpers.rs:1376-1380`, `:1493-1496`), so a
materialised inline-callee frame carried `-1` and `offset2lineno(code, -1)`
answered the `def` line.

`pyopcode.py:200 dispatch_bytecode` writes `self.last_instr` at every
bytecode; for a frame that is not `virtualizable_boxes[-1]`,
`pyjitpl.py:1120-1146 _nonstandard_virtualizable` and `:1189-1193
_opimpl_setfield_vable` make that a plain recorded `setfield_gc` on the
virtual frame. Recording it once per may-force residual boundary is that
store after the optimizer has coalesced it.

The default configuration is unchanged: the `VableEscapedDuringResidualCall`
abort still routes these shapes to the interpreter, so the recorded pc is not
yet observable in production. Measured with the abort suppressed, an inlined
callee reading `sys._getframe(0).f_lineno` goes from `[2, 4]` to `[4]`.

check.py dynasm 372/372; cargo fmt clean.

Assisted-by: Claude
… committed

`flush_active_frame_escape` returned `flushed || expected == frame as usize`,
so on the redirected (`escaped_published`) arm a declined flush reported "not
escaped" and the force never happened. Return true whenever the guard matched.

Upstream decides the escape from the vable token state alone, with no image
write involved: `virtualizable.py:231-246 tracing_after_residual_call` returns
true iff `force_now` (`:248-255`) cleared the token — "The values in the
virtualizable are always correct during tracing. We only need to reset
vable_token to TOKEN_NONE as a marker" — and `pyjitpl.py:3373-3390
vable_after_residual_call` raises ABORT_ESCAPE on that state unconditionally.
Runtime forcing also resets the token before writing fields
(`resume.py:1405-1408`). Same for a vref'd inlined callee
(`virtualref.py:157-167`).

The declined-flush bookkeeping is unchanged: the locals region is still written
(`flush_locals_region_to_frame`), no resume pc is committed, and the undo stays
armed.

No behaviour change at the default configuration — the escape flush declines 0
times across r1-r5 and the synthetic corpus, so `flushed` was already true on
every reporting path. Measured with a decline forced to fire, the abort count
returns from 0 to 10 and a side-effect-plus-frame-read callee still applies its
effect exactly once (30000/30000), i.e. the replay double-apply the previous
comment described does not occur.

check.py dynasm 372/372, cranelift 372/372; cargo fmt clean.

Assisted-by: Claude
…shed-callee disjunct

`flush_active_frame_escape` matches on `expected == frame as usize ||
escaped_published` and collapses both into one bool, so the share of the
`VableEscapedDuringResidualCall` class carried by each disjunct was unmeasured.

Adds three `fbw_diag` counters classifying each matched escape, and two more
attributing the token clear at the force site in `force_pyframe` — that clear is
what the post-residual probe reads as an escape and turns into the abort, so its
attribution is 1:1 with the aborts. Both are exported through the `MAJIT_STATS`
jit-stats block.

Measured, dynasm release, 356 synthetic fixtures:

    loops_aborted                  207
    attributed escape forces       138  (66.7% of aborts)
      by_portal                     93
      by_callee_only                45  (32.6% of the class, 21.7% of aborts)

Per fixture the attribution sums to the abort count exactly (r1 5+5=10,
r3 5+10=15), which is the check that the counters name aborts rather than calls
to the flush.

`RING_BASE` moves 6 -> 11 for the five new slots; `LEN` is derived from it, and
the wasm runner's duplicated copy is updated to match.

check.py dynasm 372/372; cargo fmt clean.

Assisted-by: Claude
… frame

The strict fresh-frame fold in `vable_ops.rs` returned `Ok(Continue)` for a
`setfield_vable` / `setarrayitem_vable` on the inline callee's own frame
register, emitting neither an IR op nor a concrete store, so the live PyFrame
kept whatever image it had at entry.

Carry the callee's concrete `PyFrame*` and its seeded frame box on
`CalleeLocalsShadow`, set both where the sub-walk resolves
`callee_portal_frame_reg`, and have both fold arms store through via
`store_live_frame_static_int` / `store_live_frame_array_slot`.

`store_live_frame_array_slot` already ignores non-Ref values and slots past
the array end, so the array arm keeps a `debug_assert!` rather than a release
panic on a value shape the corpus does not exercise.

r1-r5 answers, per-fixture abort counts and the corpus attribution
(loops_aborted=207, by_portal=93, by_callee_only=45 over 356 synth fixtures)
are unchanged; committed `.jitstats` baselines do not move.

Assisted-by: Claude
…ces the frame

Once a residual call has forced the frames, the inline callee's folded local
stores exist only in `CalleeLocalsShadow` — the trace carries no op for them,
so anything reading the callee frame after the force sees the entry image.

Add `disarm_folded_inline_callee_after_escape`, called from the `forced` arm of
`try_execute_residual_call_via_executor`: it replays the shadow's slots in
index order through `vable_setarrayitem_indexed` against the seeded callee
frame box, captures the resulting guards, and then clears `fold_frame_reg` so
subsequent stores on that frame take the ordinary recorded path.

It is inert unless the walk is an inline sub-walk with a seeded frame box whose
portal frame register still holds it.

r1-r5 answers, per-fixture abort counts and the corpus attribution
(loops_aborted=207, by_portal=93, by_callee_only=45) are unchanged; committed
`.jitstats` baselines do not move; dynasm check.py 372/372.

Assisted-by: Claude
…ts to its frame

`LiveLastInstrGuard::enter` retargeted the frame to the inline callee's
concrete frame but kept the outer walk's `vstack_cur_pypc`, so the pc and the
frame named different code.  A sub-walk never advances that mirror
(`vstack_valid` is false while `PYRE_FBW_CALLEE_VSTACK` is off), so the value
published onto the callee frame was a constant 0 and a frame reader inside the
callee reported the function's first line.

Resolve the pc against the callee's own metadata instead, via a new
`inline_callee_py_pc` that reuses the `python_pc_for_jitcode_pc` lookup
`maybe_record_inline_callee_last_instr` already performs.  The outer-frame arm
is unchanged.

Measured with the escape abort suppressed for the published-callee disjunct
(a local experiment, not part of this commit), this is what turns
`sys._getframe(0).f_lineno` inside an inlined callee from the `def` line into
the reading line, and `f_lasti` from 0 into the executing offset.

At the default configuration it is behaviour-neutral: r1-r5 answers unchanged,
corpus loops_aborted=207 / by_portal=93 / by_callee_only=45 unchanged over 356
synth fixtures, dynasm check.py 372/372, committed `.jitstats` baselines
unmoved.

Assisted-by: Claude
…ites

`fb00dfbf8ce` moved `jitcode_dispatch/diag.rs:160 python_pc_for_jitcode_pc` to
`py_coord.rs:37 containing_py_pc_for_jitcode_pc`, body byte-identical.
`inline_callee_py_pc` and `maybe_record_inline_callee_last_instr` were written
against the old name, and rebasing onto that commit produced no textual
conflict, so the tip did not compile: two E0425s at `residual_call.rs:3557`
and `:3582`. All three `prepare Charon/LLBC` jobs failed there, charon building
`pyre-jit` compiling `pyre-jit-trace`, and every downstream job skipped.

Point both sites at `crate::py_coord::containing_py_pc_for_jitcode_pc`.

check.py dynasm 372/372, cranelift 372/372, wasm 368/368, all on a re-extracted
`.ullbc`; `cargo test --all --no-default-features --features dynasm` and
`cargo fmt --all -- --check` clean; `cargo check --workspace --all-targets`
clean under `--features dynasm` and `--no-default-features --features
cranelift`.

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: 05df83cf11

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

// `pyre_jit_trace::trace::fbw_diag`.
if let Ok(fbw) = instance.get_typed_func::<u32, u64>(&mut store, "pyre_fbw_diag") {
const RING_BASE: u32 = 6;
const RING_BASE: u32 = 11;

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 Print the new WASM escape counters

When diagnostics are collected through pyre-wasm-runner, slots 6–10 are now reserved for the escape split and force attribution, but this decoder only moves RING_BASE past them and never reads or prints them. Native runs expose the same counters through the new fbw_escape_split and fbw_force_attrib lines in pyrex/src/lib.rs, so WASM MAJIT_STATS output silently omits the commit's new attribution data and cannot be compared with the native backends. Add the five slots to the WASM diagnostic output before decoding the ring.

Useful? React with 👍 / 👎.

@youknowone
youknowone merged commit 40c82e5 into main Aug 4, 2026
16 of 19 checks passed
@youknowone
youknowone deleted the single-walker branch August 4, 2026 06:19
youknowone added a commit that referenced this pull request Aug 4, 2026
The rebase onto main (through #1015 and #1019) moved structural jit-stats
on 26 synthetic fixtures. #1019 re-recorded only the eight dynasm
baselines; the cranelift/wasm baselines of the same fixtures and the
remaining drifted fixtures are re-recorded here from a 3-backend
measurement. No correctness or output change; the recorded values
reproduced exactly across the record run and a full verification run
(dynasm 373/373, cranelift 373/373, wasm 369/369 green).

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 4, 2026
… the test_asyncio hang) (#1016)

* jit: env-gated report and decline switches for the raw-color result-slot fallback

`inject_root_call_result`'s raw-color fallback is documented as load-bearing
and simultaneously wrong.  `PYRE_INJECT_RCA` reports each engagement with its
coordinate; `PYRE_INJECT_FALLBACK_DECLINE` declines the bridge compile instead
of engaging it, as the A/B control for attributing a wrong-slot injection.
Both are off in production; the gated branches are the only added code.

Assisted-by: Claude

* jit: keep constructor inlining out of parented guard snapshots

A guard emitted inside an inlined `__init__` resumed through a parented
snapshot chain ([caller paused after the CALL, `__init__` frame]), so the
blackhole delivered `__init__`'s return value — None — into the caller's CALL
dst register.  `typeobject.py descr_call` discards `__init__`'s result and
returns the instance; the flattened constructor inline has no frame that can
reconstruct that discard at a mid-`__init__` pause.

Two changes in `try_walker_inline_resolved_user_call`:

  * a constructor callee that is not strict-straight-line returns `Ok(None)`
    (residual instantiation) instead of entering the forward-branch multiframe
    path — and before the arm that would decline the enclosing key to
    interpretation;
  * `strict_seed` excludes constructors, so a straight-line `__init__` inlines
    through the ordinary single-frame path whose guards collapse to the caller
    boundary (deopt re-executes the whole instantiation call).

This was the cause of the test.test_asyncio hang/error cascade: a warmed
`BaseEventLoop._call_soon` trace inlined `events.Handle(...)` including its
branchy `__init__`; flipping the loop debug flag failed the `get_debug()` bool
guard inside the inlined body and every subsequent `call_soon` produced
`handle = None` (`AttributeError: 'NoneType' object has no attribute
'_source_traceback'`, lost wakeups, event loop parked in kevent forever).

New fixture `synth/type_call_inline_init_branch_deopt` reproduces the shape
deterministically (verified failing on the unfixed build) with converged
jitstats baselines on all three backends.

Verified: check.py dynasm 372/372, cranelift 372/372, wasm 368/368;
cpython_tests gate 47 PASS / 0 regressions (test.test_asyncio included);
full test_asyncio suite completes in ~2min (one intermittent
`_run_once` `self`-slot corruption class remains, tracked separately).

Assisted-by: Claude

* cpython_tests: widen the rust-panic reason cap to 200 chars

The 80-char cap cut a CI panic reason at
"...blackhole.rs:6", dropping the line number.  The sibling
signal/abort branch already keeps 200 chars.

Assisted-by: Claude

* check.py: re-record 38 jitstats baselines after the rebase onto main

The rebase onto main (through #1015 and #1019) moved structural jit-stats
on 26 synthetic fixtures. #1019 re-recorded only the eight dynasm
baselines; the cranelift/wasm baselines of the same fixtures and the
remaining drifted fixtures are re-recorded here from a 3-backend
measurement. No correctness or output change; the recorded values
reproduced exactly across the record run and a full verification run
(dynasm 373/373, cranelift 373/373, wasm 369/369 green).

Assisted-by: Claude

* extra_tests: satisfy the runner's OK convention in seven new parity scripts

The parity runner passes a script only when it exits 0 AND its last
stdout line is exactly "OK". Five of #950's scripts printed nothing,
one printed lowercase "ok", and newmemoryview_omitted_geometry imported
__pypy__ unconditionally, failing under the CPython reference runner.
These never ran in CI before because the check.py step ahead of the
parity step was red on main.

Assisted-by: Claude

* builtins: BaseExceptionGroup and ExceptionGroup basicsize 96 per CPython 3.14.6

CPython 3.14.6 grew PyBaseExceptionGroupObject by one pointer slot, so
__basicsize__ moved 88 -> 96 for BaseExceptionGroup and ExceptionGroup.
Update cpython314_builtin_abi and the type_members_python314 expected
table to the 3.14.6 values (measured; the two groups are the only
changed entries, flags unchanged). CI's reference CPython is 3.14.6
while local homebrew is 3.14.5, which is why this only failed in CI.

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