Skip to content

Instantly share code, notes, and snippets.

@mattip
Last active April 12, 2026 10:01
Show Gist options
  • Select an option

  • Save mattip/429841329e0eefdb1fb926ebb83c65f2 to your computer and use it in GitHub Desktop.

Select an option

Save mattip/429841329e0eefdb1fb926ebb83c65f2 to your computer and use it in GitHub Desktop.
Plan porting CPython exception table to PyPy

Exception Table Migration Plan

Architecture

Current PyPy: runtime linked list of FrameBlock objects (ExceptBlock, FinallyBlock, SysExcInfoRestorer) anchored at frame.lastblock. The compiler emits SETUP_FINALLY/SETUP_EXCEPT/POP_BLOCK to manage this list. On exception, unrollstack() walks the list to find the handler.

CPython 3.11: no block stack at all. The exception table co_exceptiontable maps bytecode offset ranges to (handler_offset, stack_depth, lasti_flag). Only consulted when an exception actually fires — zero overhead on the happy path.


JIT Compatibility

Adopting CPython's model might improve JIT output. Three reasons:

  1. SETUP_FINALLY/POP_BLOCK currently generate virtual ExceptBlock/FinallyBlock allocations that the JIT eliminates. With the table model those allocations disappear entirely — nothing to eliminate.
  2. lastblock is currently in PyFrame._virtualizable_, requiring the JIT to track it at every escape point. It can be removed, simplifying the virtualizable save/restore machinery.
  3. Exception dispatch (unrollstack) happens outside the traced fast path anyway (it's the guard-failure/blackhole path). Replacing it with a table scan doesn't touch JIT-compiled code at all.

Implementation Phases

Phase What Risk Scope
0 Add co_exceptiontable to PyCode, encoder in assemble.py Low Additive
1 PUSH_EXC_INFO, CHECK_EXC_MATCH opcodes Medium New opcodes
2 handle_operation_error → table lookup, update RERAISE High Core dispatch
3 Compiler: stop emitting SETUP_FINALLY/POP_BLOCK, emit table entries High Largest change surface
4 JIT: remove lastblock from _virtualizable_, update trace tests Low Once 2–3 stable
5 fset_f_lineno: replace markblocks/compatible_block_stack with table-based validation Medium Fixes the settrace failures
6 Remove dead code (FinallyBlock, ExceptBlock, SETUP_FINALLY, etc.) Low Cleanup

Critical constraint: Phases 2 and 3 must be developed in lockstep — compiler output must exactly match the new interpreter expectations. Cannot be done incrementally without a feature flag to run both models in parallel.

Highest-risk point: handle_operation_error rewrite. It has subtle interactions with generators, coroutines, sys.exc_info() save/restore, the hidden_operationerr mechanism, and JIT blackhole behavior.

Estimated complexity: Multi-month project. The compiler changes alone (all try/except/finally/with/except* patterns in codegen.py) are the bulk of the work.

The work will take place on an exceptiontable branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment