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.
Adopting CPython's model might improve JIT output. Three reasons:
SETUP_FINALLY/POP_BLOCKcurrently generate virtualExceptBlock/FinallyBlockallocations that the JIT eliminates. With the table model those allocations disappear entirely — nothing to eliminate.lastblockis currently inPyFrame._virtualizable_, requiring the JIT to track it at every escape point. It can be removed, simplifying the virtualizable save/restore machinery.- 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.
| 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.