Skip to content

jit: exempt LOAD_FAST_CHECK's null arm from the loop-body abort_permanent scan, and give markers an exact jitcode -> Python-PC inverse - #1023

Merged
youknowone merged 6 commits into
mainfrom
wasm-jit
Aug 4, 2026
Merged

youknowone merged 6 commits into
mainfrom
wasm-jit

Conversation

@youknowone

@youknowone youknowone commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

Six commits. The centerpiece is a JIT decline that refused whole frames over a
dead-end block no bound-local run ever reaches; the rest are the fixtures that
pin it, a doc correction, a dead error kind, a CI job merge, and a diagnostic.

jit: exempt LOAD_FAST_CHECK's null arm from the loop-body abort_permanent scan

loop_body_abort_permanent_pc declined a whole frame on any
abort_permanent in its scan window. That refusal exists for unported
opcodes, whose arm bails without modelling stack effect — a mis-seeded loop
guard exits on pass 1 and double-runs the tail.

LOAD_FAST_CHECK is ported. It lowers to a ptr_nonzero split: the codewriter
compiles the bound arm normally and sends the null arm to a dead-end block
ending in emit_abort_permanent!. Neither walk outcome mis-seeds anything, so
the marker is exempt — and exempt everywhere in the window, not just in the
tail. (loop_end, the max back-edge jitcode pc, is not a source-position proxy:
loop_in_try_tail_raise_and_second_loop's source-tail marker measures
2067 < loop_end 3363.)

Naming the owning opcode is the whole difficulty. py_floor_by_jit_pc is built
from each Python PC's first jitcode offset, and the null arm's block is
emitted after the whole body, so the floor lookup resolves it to whichever PC
last opened a segment — RETURN_VALUE, not the LOAD_FAST_CHECK that owns it.

So markers get an exact inverse. SSARepr::abort_permanent_insn_pos records
every marker's insns index at serialize_op; it is carried through the three
-live- splicing passes and the remove_repeated_live remap, translated in the
finish_with_positions_from batch alongside pc_map, and published as
PyJitCodeMetadata::abort_permanent_py_pc_by_jit_pc. Only markers get this —
there are a handful per jitcode, and they are the one op class a whole-frame
decision hangs on.

SSARepr::stream_positions_mut replaces the per-table position-rewrite loops in
the splicing passes, so a newly added side table cannot silently miss a pass.
That hand-off is five sites, and missing any one leaves the table empty at
runtime with no error.

Result — the three new fixtures, on all three backends:

fixture loops_compiled guard_failures min-of-9 CPU (dynasm)
loop_in_try_raise_into_handler 1 → 3 20 → 60 0.65s → 0.19s
loop_in_try_tail_raise_and_second_loop 1 → 4 20 → 80 1.33s → 0.20s
loop_in_try_tail_unbound_check 1 → 5 20 → 100 0.97s → 0.18s

loops_aborted=0, bridges_compiled=0 throughout. Every field is identical
across dynasm, cranelift and wasm
, and output is byte-exact against pypy3 and
python3.14.

bench: cover the loop_in_try abort_permanent scan widening

The widening (scan_end = code.len() when the loop header is covered by an
exception handler) had no fixture responding to it: sweeping all 356
synthetic fixtures with the scan narrowed left loops_compiled,
bridges_compiled, loops_aborted and guard_failures unchanged on every one.

Three fixtures for the shape. Each has a hot loop inside a try and a post-loop
tail reading the loop variable — which the compiler spells LOAD_FAST_CHECK
precisely because the raise can reach the handler before the loop assigns the
slot. Delivery flavours covered: builtin container, int-specialized arithmetic
guard, non-matching inner handler, inlined callee, a raise in the post-loop
tail, a second loop after the handler, and two loops under one try.

PYRE_FBW_LOOPBODY_SCAN_LOOP_ONLY drops the widening without a rebuild (the
counterpart of the existing PYRE_FBW_LOOPBODY_SCAN_FULL). With it set, all
three fixtures fail guard_failures 20 -> 40/60/80 — the fixtures gate the
widening in both directions.

jit: record what the loop_in_try scan widening actually declines

The doc comment named "the DELETE_FAST cleanup for except X as e" as the
post-loop marker the widened scan catches. Across every shape these fixtures
cover, it is LOAD_FAST_CHECK's null arm instead. Comment only; landed before
the fix so the wrong attribution is not what the fix appears to rest on.

interp: remove the never-constructed GeneratorReturn error kind

PyErrorKind::GeneratorReturn had six references — the variant, its
ExcKind::RuntimeError mapping, three handlers in eval_loop /
handle_exception, and a comment — and no constructor. The handlers were
unreachable. Instruction::ReturnGenerator does not unwind: return_generator
is a bare push(w_none()) for the following POP_TOP, because the generator
object is already built at call time in call_user_function_with_eval.

ci: build the pyre-wasm web binding inside the Linux check.py job

Drops the separate wasm-build matrix job; the Linux check.py job builds
-p pyre-wasm --features web before check.py's own wasm-host build, so both
bindings share the release target directory instead of paying a second checkout,
cache restore and LLBC download.

jit: name the opcode that owns a declining abort_permanent in PYRE_FBW_DEBUG_ABORT

[fbw-abort] reported a bare jitcode offset; it now resolves through the new
inverse and appends py_pc=<n> <Instruction>. One line instead of a
MAJIT_DUMP_SSAREPR + last_instr = py_pc - 1 hunt.

⚠️ Honest caveat: no fixture in the synthetic suite currently reaches this
decline, so the line is only observable on a routing-patched binary or under
PYRE_FBW_LOOPBODY_SCAN_FULL=1. It is gated behind an env var check.py never
sets.

Verification

  • cargo test --all --no-default-features --features dynasm — pass (101 test
    binaries).
  • cargo fmt --check — clean.
  • The three new fixtures are green on dynasm, cranelift and wasm.

check.py at the measurement base (origin/main@40c82e5c6a9) had 8 dynasm /
16 cranelift / 14 wasm failures. All 38 are inherited, not introduced.
Proven, not asserted: a control binary was built with only this branch's five
source files reverted to origin/main, on both dynasm and cranelift, with LLBC
re-extracted per leg and the fingerprint checked (pyre-jit re-extracted each
swap; the other crates correctly skipped). Every affected fixture's jitstats
came back 26/26 byte-identical to the control, while the positive control —
this branch's three fixtures — correctly differed. #1019 subsequently
re-recorded exactly the eight dynasm baselines flagged here, confirming the
attribution independently.

The branch is two commits behind origin/main as of opening (#1018, #945, both
of which re-recorded a large set of baselines). git merge-tree reports a clean
merge; re-verification against the newer base has not been run, so the failure
counts above are stated against the base they were measured on.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of generator returns and exception paths in loops.
    • Increased reliability for loops interrupted by exceptions and cases involving conditionally bound variables.
  • Performance

    • Enhanced JIT tracking and optimization for exception-heavy loop execution.
  • Testing

    • Added benchmark coverage for exception handling, loop control flow, and unbound-variable scenarios across supported execution targets.

`loop_body_abort_permanent_pc` scans to `code.len()` instead of the loop's
back edge when the loop header is covered by an exception handler.  No
synthetic fixture responded to that widening: sweeping all 356 with the scan
narrowed left `loops_compiled`, `bridges_compiled`, `loops_aborted` and
`guard_failures` unchanged on every one.

Three fixtures for the shape.  Each has a hot loop inside a `try` and a
post-loop tail that reads the loop variable, which the compiler spells
LOAD_FAST_CHECK because the raise can reach the handler before the loop
assigns it.  LOAD_FAST_CHECK splits on a conditionally-bound slot and its null
arm emits the `abort_permanent` the widened scan finds; the marker sits in a
dead-end block reached only when the local really is unbound, and
`unbound_tail` is the arm that reaches it.  Delivery flavours covered: builtin
container, int-specialized arithmetic guard, non-matching inner handler,
inlined callee, a raise in the post-loop tail, a second loop after the
handler, and two loops under one `try`.

`PYRE_FBW_LOOPBODY_SCAN_LOOP_ONLY` drops the widening without a rebuild,
the counterpart of the existing `PYRE_FBW_LOOPBODY_SCAN_FULL`.  With it set,
all three fixtures fail `jit-stats regression: guard_failures 20 -> 40/60/80`.

Output is byte-identical across pypy3, python3.14 and all three backends, and
the recorded baselines agree field-for-field across dynasm, cranelift and wasm.

Assisted-by: Claude
`loop_body_abort_permanent_pc`'s doc comment names "the `DELETE_FAST` cleanup
for `except X as e`" as the post-loop marker the widened scan catches.  Across
the shapes `bench/synth/loop_in_try_*` cover, every marker it catches is
`LOAD_FAST_CHECK`'s null arm instead.

A loop inside a `try` is what makes the tail read a loop variable through
LOAD_FAST_CHECK rather than LOAD_FAST — the raise can reach the handler before
the loop assigns the slot, so the slot is only conditionally bound on the join.
The codewriter splits that opcode, compiles the bound arm, and sends the null
arm to a dead-end block whose `abort_permanent` no bound-local run reaches.

Also records why the marker cannot simply be exempted: the null arm's block is
emitted after the whole body, and `py_floor_by_jit_pc` keys each Python PC to
its first jitcode offset, so the floor lookup attributes the late block to
`RETURN_VALUE` rather than to the LOAD_FAST_CHECK that owns it.

Comment only; no behaviour change.

Assisted-by: Claude
`PyErrorKind::GeneratorReturn` had six references in the tree: the variant, its
`ExcKind::RuntimeError` mapping, three handlers in `eval_loop` /
`handle_exception`, and a comment.  No constructor.  Nothing ever raised it, so
the handlers were unreachable.

`Instruction::ReturnGenerator` does not unwind: `return_generator`
(eval.rs:4375) is a bare `push(w_none())` for the following POP_TOP, because
the generator object is already built at call time in
`call_user_function_with_eval`.

Verified with cargo test --all --no-default-features --features dynasm (7396
tests), check.py --synthetic-only --backend dynasm (359/359), and the
cpython_tests dynasm legs for test_generators, test_yield_from,
test_coroutines, test_generator_stop and test_exceptions (no regressions).

Assisted-by: Claude
…nent scan

`loop_body_abort_permanent_pc` declined a whole frame on any
`abort_permanent` in its scan window.  `LOAD_FAST_CHECK` lowers to a
`ptr_nonzero` split whose null arm ends in a dead-end block carrying that
marker, so the scan refused frames over a conditionally-bound local read.
Skip markers whose owning opcode is `LoadFastCheck`.

Naming the owning opcode needs an exact jitcode-offset -> Python-PC
inverse: `py_floor_by_jit_pc` keys each Python PC to its FIRST jitcode
offset, and the null arm's block is emitted after the whole body, so the
floor lookup resolves it to whichever PC last opened a segment.  Record
every `abort_permanent`'s `insns` index in `SSARepr` at `serialize_op`,
carry it through the three `-live-` splicing passes and the
`remove_repeated_live` remap, translate it in the
`finish_with_positions_from` batch alongside `pc_map`, and publish it as
`PyJitCodeMetadata::abort_permanent_py_pc_by_jit_pc`.

`SSARepr::stream_positions_mut` replaces the per-table position rewrite
loops in the splicing passes.

Re-recorded the three `loop_in_try_*` baselines on all three backends:
`loops_compiled` 1 -> 3/4/5, `guard_failures` 20 -> 60/80/100, identical
across dynasm/cranelift/wasm.  Output stays byte-exact against pypy3 and
python3.14.  Min-of-9 user+sys CPU, dynasm: 0.65s -> 0.19s,
1.33s -> 0.20s, 0.97s -> 0.18s.

Assisted-by: Claude
Drops the separate `wasm-build` matrix job. The Linux job that runs
pyre/check.py now builds `-p pyre-wasm --features web` before check.py's
own wasm-host build, so both bindings share the release target directory
instead of paying a second checkout, cache restore and LLBC download.

Assisted-by: Claude
…W_DEBUG_ABORT

The `[fbw-abort] start_pc=… abort_permanent_pc=…` line reported a bare
jitcode offset. Resolve it through `abort_permanent_py_pc_by_jit_pc` and
append `py_pc=<n> <Instruction>`.

`marker_is_load_fast_check_null_arm`'s table lookup and CodeObject decode
move into `abort_permanent_owner`, which both callers share; the predicate
becomes a `matches!` on its result.

The line only prints under `PYRE_FBW_DEBUG_ABORT`, which check.py never
sets, and no fixture in the synthetic suite currently reaches this decline.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ce74dbac-f23e-4027-a1e3-8650109cc825

📥 Commits

Reviewing files that changed from the base of the PR and between a8f3cdb and 072fd2c.

📒 Files selected for processing (20)
  • .github/workflows/pyre-ci.yml
  • pyre/bench/synth/loop_in_try_raise_into_handler.cranelift.jitstats
  • pyre/bench/synth/loop_in_try_raise_into_handler.dynasm.jitstats
  • pyre/bench/synth/loop_in_try_raise_into_handler.py
  • pyre/bench/synth/loop_in_try_raise_into_handler.wasm.jitstats
  • pyre/bench/synth/loop_in_try_tail_raise_and_second_loop.cranelift.jitstats
  • pyre/bench/synth/loop_in_try_tail_raise_and_second_loop.dynasm.jitstats
  • pyre/bench/synth/loop_in_try_tail_raise_and_second_loop.py
  • pyre/bench/synth/loop_in_try_tail_raise_and_second_loop.wasm.jitstats
  • pyre/bench/synth/loop_in_try_tail_unbound_check.cranelift.jitstats
  • pyre/bench/synth/loop_in_try_tail_unbound_check.dynasm.jitstats
  • pyre/bench/synth/loop_in_try_tail_unbound_check.py
  • pyre/bench/synth/loop_in_try_tail_unbound_check.wasm.jitstats
  • pyre/pyre-interpreter/src/error.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-jit-trace/src/pyjitcode.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit/src/jit/codewriter.rs
  • pyre/pyre-jit/src/jit/flatten.rs
💤 Files with no reviewable changes (1)
  • pyre/pyre-interpreter/src/error.rs

Walkthrough

The PR standardizes generator-return exception dispatch, preserves abort-permanent JIT positions through code generation, resolves marker owners during tracing, adds exception-loop benchmarks, and moves the WebAssembly web-binding build into Linux CI.

Changes

JIT exception handling

Layer / File(s) Summary
Standardize generator-return exception dispatch
pyre/pyre-interpreter/src/error.rs, pyre/pyre-interpreter/src/eval.rs
GeneratorReturn is removed from PyErrorKind and uses standard exception handling in trace and opcode paths.
Emit abort-permanent position metadata
pyre/pyre-jit/src/jit/codewriter.rs, pyre/pyre-jit-trace/src/pyjitcode.rs, pyre/pyre-jit-trace/src/state.rs
Instruction remapping preserves abort-permanent positions and emits JIT-offset-to-Python-PC metadata.
Resolve abort-permanent loop markers
pyre/pyre-jit-trace/src/trace.rs
Loop tracing scans configured regions, filters LOAD_FAST_CHECK null-arm markers, and reports resolved Python owners.
Add exception-loop benchmark coverage
pyre/bench/synth/loop_in_try_*.py, pyre/bench/synth/loop_in_try_*.jitstats
Synthetic benchmarks cover exception exits, subsequent loops, conditional local binding, and backend JIT statistics.

Linux WebAssembly CI

Layer / File(s) Summary
Integrate the WebAssembly web build
.github/workflows/pyre-ci.yml
pyre-check-linux builds the WebAssembly web binding before running check.py; the standalone wasm-build job is removed.

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant PythonCode
  participant PyJitCodeWriter
  participant PyJitCodeMetadata
  participant TraceScanner
  PythonCode->>PyJitCodeWriter: compile loop with abort-permanent markers
  PyJitCodeWriter->>PyJitCodeMetadata: store JIT offsets and Python PCs
  PyJitCodeMetadata->>TraceScanner: provide marker ownership metadata
  TraceScanner->>TraceScanner: scan loop body or widened try tail
  TraceScanner-->>PythonCode: report resolved abort diagnostics
Loading

Poem

A rabbit hops through loops of code,
While exception paths are mapped and shown.
JIT markers leave a trail so neat,
Web builds share their cached retreat.
Benchmarks thump with stats anew—
“Hop hop!” says Bunny, “the flow is true!”

🚥 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 and concisely describes the main change: exempting LOAD_FAST_CHECK null-arm markers and adding an exact JIT-to-Python-PC mapping.
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 wasm-jit

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/072fd2cc303729ae6391f56e4c4f8d8985bac476/pyre-jit-trace/src/trace.rs#L4853-L4856
P1 Badge Use the exact Python PC when resuming exempted markers

When a hot loop reaches a post-loop LOAD_FAST_CHECK whose conditionally bound local is actually null, this exemption allows the walk to reach the marker reactively. However, the resulting AbortPermanentMarkerReached is still translated by fbw_abort_resume_py_pc through containing_py_pc_for_jitcode_pc, even though the new metadata explicitly documents that this floor lookup attributes the late null-arm block to an unrelated opcode. The abort-state flush can therefore resume at the wrong Python instruction, skipping or re-executing bytecode instead of raising UnboundLocalError; the reactive abort path needs to consume abort_permanent_py_pc_by_jit_pc too. This violates the repository invariant that generated JIT behavior preserve interpreter semantics.

AGENTS.md reference: AGENTS.md:L16-L19

ℹ️ About Codex in GitHub

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

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

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

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

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 072fd2c).
Updated: 2026-08-04T09:31:58.233Z

Files in the reviewed diff
.github/workflows/pyre-ci.yml
pyre/bench/synth/loop_in_try_raise_into_handler.py
pyre/bench/synth/loop_in_try_tail_raise_and_second_loop.py
pyre/bench/synth/loop_in_try_tail_unbound_check.py
pyre/pyre-interpreter/src/error.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-jit-trace/src/pyjitcode.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-jit/src/jit/codewriter.rs
pyre/pyre-jit/src/jit/flatten.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

None.

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

None.

4. Structural adaptations

  • pyre/pyre-jit/src/jit/codewriter.rs:12202 ↔ pypy/interpreter/pyopcode.py:536: pyre’s CPython-3.14 LOAD_FAST_CHECK split has a bound compiled arm and an interpreter bailout arm; PyPy 3.11 has only LOAD_FAST, which raises directly for an unbound slot. This is an opcode/version adaptation.

  • pyre/pyre-jit-trace/src/trace.rs:4756 ↔ rpython/jit/metainterp/pyjitpl.py:2784: pyre’s pre-walk abort_permanent screening is specific to its source-translated full-body walker; PyPy aborts dynamically through SwitchToBlackhole. The new exemption is correctly limited to the ported LOAD_FAST_CHECK dead arm.

  • pyre/pyre-jit-trace/src/pyjitcode.rs:92 ↔ rpython/jit/codewriter/assembler.py:41: pyre stores an explicit JitCode-offset-to-CPython-PC inverse for bailout markers. RPython assembles directly from its flow graph and retains instruction positions, so it needs no CPython-bytecode provenance table.

@youknowone
youknowone merged commit 83abf4d into main Aug 4, 2026
19 of 22 checks passed
@youknowone
youknowone deleted the wasm-jit branch August 4, 2026 13:42
youknowone added a commit that referenced this pull request Aug 4, 2026
…nent scan, and give markers an exact jitcode -> Python-PC inverse- #1023 (#1032)

* bench: cover the loop_in_try abort_permanent scan widening

`loop_body_abort_permanent_pc` scans to `code.len()` instead of the loop's
back edge when the loop header is covered by an exception handler.  No
synthetic fixture responded to that widening: sweeping all 356 with the scan
narrowed left `loops_compiled`, `bridges_compiled`, `loops_aborted` and
`guard_failures` unchanged on every one.

Three fixtures for the shape.  Each has a hot loop inside a `try` and a
post-loop tail that reads the loop variable, which the compiler spells
LOAD_FAST_CHECK because the raise can reach the handler before the loop
assigns it.  LOAD_FAST_CHECK splits on a conditionally-bound slot and its null
arm emits the `abort_permanent` the widened scan finds; the marker sits in a
dead-end block reached only when the local really is unbound, and
`unbound_tail` is the arm that reaches it.  Delivery flavours covered: builtin
container, int-specialized arithmetic guard, non-matching inner handler,
inlined callee, a raise in the post-loop tail, a second loop after the
handler, and two loops under one `try`.

`PYRE_FBW_LOOPBODY_SCAN_LOOP_ONLY` drops the widening without a rebuild,
the counterpart of the existing `PYRE_FBW_LOOPBODY_SCAN_FULL`.  With it set,
all three fixtures fail `jit-stats regression: guard_failures 20 -> 40/60/80`.

Output is byte-identical across pypy3, python3.14 and all three backends, and
the recorded baselines agree field-for-field across dynasm, cranelift and wasm.

Assisted-by: Claude

* jit: record what the loop_in_try scan widening actually declines

`loop_body_abort_permanent_pc`'s doc comment names "the `DELETE_FAST` cleanup
for `except X as e`" as the post-loop marker the widened scan catches.  Across
the shapes `bench/synth/loop_in_try_*` cover, every marker it catches is
`LOAD_FAST_CHECK`'s null arm instead.

A loop inside a `try` is what makes the tail read a loop variable through
LOAD_FAST_CHECK rather than LOAD_FAST — the raise can reach the handler before
the loop assigns the slot, so the slot is only conditionally bound on the join.
The codewriter splits that opcode, compiles the bound arm, and sends the null
arm to a dead-end block whose `abort_permanent` no bound-local run reaches.

Also records why the marker cannot simply be exempted: the null arm's block is
emitted after the whole body, and `py_floor_by_jit_pc` keys each Python PC to
its first jitcode offset, so the floor lookup attributes the late block to
`RETURN_VALUE` rather than to the LOAD_FAST_CHECK that owns it.

Comment only; no behaviour change.

Assisted-by: Claude

* interp: remove the never-constructed GeneratorReturn error kind

`PyErrorKind::GeneratorReturn` had six references in the tree: the variant, its
`ExcKind::RuntimeError` mapping, three handlers in `eval_loop` /
`handle_exception`, and a comment.  No constructor.  Nothing ever raised it, so
the handlers were unreachable.

`Instruction::ReturnGenerator` does not unwind: `return_generator`
(eval.rs:4375) is a bare `push(w_none())` for the following POP_TOP, because
the generator object is already built at call time in
`call_user_function_with_eval`.

Verified with cargo test --all --no-default-features --features dynasm (7396
tests), check.py --synthetic-only --backend dynasm (359/359), and the
cpython_tests dynasm legs for test_generators, test_yield_from,
test_coroutines, test_generator_stop and test_exceptions (no regressions).

Assisted-by: Claude

* jit: exempt LOAD_FAST_CHECK's null arm from the loop-body abort_permanent scan

`loop_body_abort_permanent_pc` declined a whole frame on any
`abort_permanent` in its scan window.  `LOAD_FAST_CHECK` lowers to a
`ptr_nonzero` split whose null arm ends in a dead-end block carrying that
marker, so the scan refused frames over a conditionally-bound local read.
Skip markers whose owning opcode is `LoadFastCheck`.

Naming the owning opcode needs an exact jitcode-offset -> Python-PC
inverse: `py_floor_by_jit_pc` keys each Python PC to its FIRST jitcode
offset, and the null arm's block is emitted after the whole body, so the
floor lookup resolves it to whichever PC last opened a segment.  Record
every `abort_permanent`'s `insns` index in `SSARepr` at `serialize_op`,
carry it through the three `-live-` splicing passes and the
`remove_repeated_live` remap, translate it in the
`finish_with_positions_from` batch alongside `pc_map`, and publish it as
`PyJitCodeMetadata::abort_permanent_py_pc_by_jit_pc`.

`SSARepr::stream_positions_mut` replaces the per-table position rewrite
loops in the splicing passes.

Re-recorded the three `loop_in_try_*` baselines on all three backends:
`loops_compiled` 1 -> 3/4/5, `guard_failures` 20 -> 60/80/100, identical
across dynasm/cranelift/wasm.  Output stays byte-exact against pypy3 and
python3.14.  Min-of-9 user+sys CPU, dynasm: 0.65s -> 0.19s,
1.33s -> 0.20s, 0.97s -> 0.18s.

Assisted-by: Claude

* ci: build the pyre-wasm web binding inside the Linux check.py job

Drops the separate `wasm-build` matrix job. The Linux job that runs
pyre/check.py now builds `-p pyre-wasm --features web` before check.py's
own wasm-host build, so both bindings share the release target directory
instead of paying a second checkout, cache restore and LLBC download.

Assisted-by: Claude

* jit: name the opcode that owns a declining abort_permanent in PYRE_FBW_DEBUG_ABORT

The `[fbw-abort] start_pc=… abort_permanent_pc=…` line reported a bare
jitcode offset. Resolve it through `abort_permanent_py_pc_by_jit_pc` and
append `py_pc=<n> <Instruction>`.

`marker_is_load_fast_check_null_arm`'s table lookup and CodeObject decode
move into `abort_permanent_owner`, which both callers share; the predicate
becomes a `matches!` on its result.

The line only prints under `PYRE_FBW_DEBUG_ABORT`, which check.py never
sets, and no fixture in the synthetic suite currently reaches this decline.

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