Exceptions¶
Exception behavior is part of the contract: divide(1, 0) raises
ZeroDivisionError and at([10, 20, 30], 9) raises IndexError at the
same point, with the same type, whichever path runs them.
Run it¶
What it prints¶
python errors.ppy, ppy errors.ppy, ppy run errors.ppy
A guard where Python would raise¶
Every place the program must be handed back to CPython is a core.guard in
the IR: a zero divisor, an index past the end, a shift past the word. Native
code takes the guard's failure edge, and the wrapper re-runs the Python
body, which raises what Python raises.
safe_divide checks b == 0 itself and never fails a guard. divide does
not, and its ZeroDivisionError comes from the fallback.
The optimizer keeps the order¶
Optimization passes do not move or drop an operation that can raise unless
they can prove it cannot. Raising is an effect (may_raise) the passes read
like any other, so an IndexError on the third element still happens after
the print of the second.
Where the code comes from¶
errors.ppy is hand-written; there is no .py source and no conversion step.
Read on: Effects and purity ยท Numerics
18_errors/errors.ppy¶
import ppy
@ppy.pure
@ppy.opt(3)
def safe_divide(a: int, b: int) -> int:
if b == 0:
return 0
return a // b
@ppy.opt(3)
def divide(a: int, b: int) -> int:
return a // b
@ppy.opt(3)
def at(values: list[int], index: int) -> int:
return values[index]
def main() -> None:
print(safe_divide(7, 2), safe_divide(7, 0))
print(divide(7, 2), divide(-7, 2))
try:
divide(1, 0)
except ZeroDivisionError as error:
print("caught", type(error).__name__)
print(at([10, 20, 30], 1))
try:
at([10, 20, 30], 9)
except IndexError as error:
print("caught", type(error).__name__)
main()
Source: examples/18_errors.