Catch the exception a program raised - #101
Merged
Merged
Conversation
`try` with all four clauses: `except`, `except E as e`, `except (A, B)`, a bare `except`, `else` and `finally`, nested inside each other and inside loops and functions. The `except` clauses are desugared in the lowerer into a chain of ifs over one slot, ending in a `raise` of that slot so an exception nothing matched carries on out. That puts the order the clauses are tried in, and the fall-through, in the printed HIR rather than in a rule only the interpreter knows. `finally` is compiled twice, once for the way out that worked and once for the way out that did not, which is what CPython does and which means the interpreter never has to remember an interrupted action. A `return`, a `break` or a `continue` leaving a `try` pops its handler entries and replays every `finally` it is leaving, and a `return` settles its value first so `try: return x finally: x = 99` returns what `x` was. The interpreter grew a handler stack per frame and three instructions. Every other instruction still returns its failure rather than jumping, and the loop around them is the single place that catches one. `__context__` and a fully dynamic bare `raise` are a follow-up, and both are written down where they are missing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
trywith all four of its clauses:except,except E as e,except (A, B), a bareexcept,elseandfinally, nested inside each other and inside loops and functions. This is the second half of the exception work; #100 wasraise.What it does
A clause catches its class and everything under it, so
except ArithmeticErrorcatches aZeroDivisionError, and an exception nothing matched carries on out to whatever is outside the statement.except Exceptiondoes not catch aKeyboardInterruptor aSystemExit, which is the hierarchy from the last PR finally being read by something. A clause naming something that is not an exception class raisesTypeError: catching classes that do not inherit from BaseException is not allowed, and a tuple inside a tuple is that same mistake, verified against 3.14 rather than remembered.What a handler binds is the object that was raised, so
raise eand theexcept ... as ecatching it are the same object. One that never was an object grows one when it is caught, since a division by zero raises out of Rust andexcept ZeroDivisionError as estill has to bind something.astakes the name away again at the end of the clause, and the cleanup is itself afinallyso it happens even when the handler raises.Three decisions worth reviewing
The
exceptchain is desugared in the lowerer rather than carried as a handler list into the HIR. Anexceptclause is a test and a body and the HIR already has a statement for that. The chain ends in araiseof the slot, so the try order and the fall-through are visible inkohebi hiroutput instead of living in a rule only the interpreter knows.finallyis compiled twice, once for the way out that worked and once for the way out that did not. That is what CPython does and it is shorter than teaching the interpreter to remember what it was in the middle of.break,continueandreturninside atrycannot just jump: they pop their handler entries and replay everyfinallythey are leaving. Areturnsettles its value first, sotry: return x finally: x = 99returns whatxwas, and areturnwritten inside afinallyreplays the ones outside it and not itself.The VM catches in one place. A frame keeps a handler stack; every instruction still returns its failure rather than jumping, and the loop around them pops a handler, materialises the exception into its register and jumps. A frame with nothing on its stack hands the exception to its caller, so catching what a call three frames down raised needs no extra machinery.
Deliberately not here
__context__, so an exception raised while handling another prints on its own rather than under CPython'sDuring handling of the above exception, another exception occurred. And a bareraiseis resolved where it is written rather than where it runs, soexcept: raiseworks but a bareraisein a function called from a handler still saysNo active exception to reraise. Both are the same follow-up and both are written down in the module docs where they are missing.Checking
21 new interpreter and lowering tests, 4 bytecode layout tests, 3 core unit tests and 2 CLI tests. 55 differential programs run against a live CPython 3.14.7 match on output and exit code, apart from
strnot being a builtin yet and the__context__preamble above.