Commit eeeb42c
Add
This introduces an expression-level fallback for exceptions — the thing `dict.get()` does for one specific case, generalized to any fallible expression: `port = int(os.environ["PORT"]) perchance 8080` evaluates the left operand and, if it raises an exception matching the guard (default: `Exception`, never `BaseException`), yields the fallback instead. An explicit guard is spelled `first = seq[0] perchance None from IndexError`, accepting anything an `except` clause accepts. The fallback and guard are lazily evaluated only when an exception occurs, so it short-circuits like `or` but over exceptions rather than falsiness, and chains left-associatively: `d["a"] perchance d["b"] perchance "default"`. This is PEP 463 (exception-catching expressions) with the syntactic objections fixed: no colon inside the expression, no parentheses required, and it reads as English at exactly the point where the program acknowledges chance. The killer use case is comprehensions, where a fallible step today forces a named helper function or an inner try statement: `[int(line) perchance None from ValueError for line in text.splitlines()]`.
There is also a statement form, `perchance FileNotFoundError, PermissionError: os.unlink(tmp)`, which is sugar for `try:`/`except (...): pass` — the pattern the stdlib alone spells out ~1360 times. The exception list is mandatory: there is no bare `perchance:`, so swallowing everything requires writing `perchance Exception:` and owning it in review.
Design decisions worth noting: the exception object is not bindable (no `as`) — if you need it, you need a try statement; this is the lambda principle, keeping the expression form too small to hide logic in. The default guard is loaded via a new `LOAD_COMMON_CONSTANT` slot (`CONSTANT_EXCEPTION`) rather than a name lookup, so a local `Exception = ...` cannot change what gets caught. A swallowed exception rides along as `__context__` if the fallback itself raises. Mixing with a conditional expression requires parentheses. `perchance` is a soft keyword like `match`: juxtaposed NAME NAME is a syntax error today, so no existing code changes meaning, and `perchance` remains usable as an identifier — `x = perchance perchance 0` parses and means what you'd hope.
Implementation: new `Perchance(expr value, expr fallback, expr? guard)` node in Python.asdl; a left-recursive `perchance_expression` grammar rule next to `if_expression`; the statement form is desugared to `Try` directly in the parser action, so it needs no new statement node, symtable, or codegen support. Code generation mirrors `codegen_try_except` and uses the zero-cost exception tables: the happy path executes no extra instructions, with the handler reachable only through the exception table. Both forms are gated with `CHECK_VERSION(16)`. Known wart for a real version: in `raise f() perchance g() from cause`, the `from` binds to the perchance guard, not the raise cause — parenthesize to disambiguate; dedicated invalid-syntax rules with helpful errors are left for later, as are docs and tests.
To sleep, perchance to dream.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>perchance, a soft keyword for exception-tolerant expressions (PoC)1 parent 4f3be1b commit eeeb42c
16 files changed
Lines changed: 2438 additions & 1799 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
| 146 | + | |
146 | 147 | | |
147 | 148 | | |
148 | 149 | | |
| |||
481 | 482 | | |
482 | 483 | | |
483 | 484 | | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
484 | 500 | | |
485 | 501 | | |
486 | 502 | | |
| |||
719 | 735 | | |
720 | 736 | | |
721 | 737 | | |
| 738 | + | |
722 | 739 | | |
723 | 740 | | |
724 | 741 | | |
725 | 742 | | |
726 | 743 | | |
727 | 744 | | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
728 | 750 | | |
729 | 751 | | |
730 | 752 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
| 85 | + | |
| 86 | + | |
86 | 87 | | |
87 | 88 | | |
88 | 89 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
772 | 772 | | |
773 | 773 | | |
774 | 774 | | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
775 | 786 | | |
776 | 787 | | |
777 | 788 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
47 | | - | |
| 47 | + | |
| 48 | + | |
48 | 49 | | |
49 | 50 | | |
50 | 51 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| 66 | + | |
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
| |||
0 commit comments