Exception handing in JIT: unwind-based vs. return-based approach?
- Dominant language
- Python
- Stars
- 16.8k
- Forks
- 603
- Avg merge
- 4d 23h
- Merged PRs (30d)
- 6
Description
Hi there!
I'd like to open a discussion around the current exception-handling mechanism under the **JIT path**, and propose an alternative that could simplify both the implementation and future extensibility.
### Current Behavior
When an unhandled Codon exception is raised in a JIT-compiled function, the control flow goes through **two layers of exception throwing**:
```
Codon exception (e.g. raise IndexError)
→ seq_throw()
→ _Unwind_RaiseException(SEQ_EXCEPTION_CLASS)
→ no matching handler found → fall through
→ seq_terminate()
→ AOT path: print diagnostics and call abort()
→ JIT path: throw runtime::JITError
```
For example, the following snippet:
```python
import codon
@codon.jit
def func():
x = []
x.pop() # raise IndexError
func()
```
produces output like:
```
Traceback (most recent call last):
...
...codon_jit.JITError: IndexError: /xxx/stdlib/internal/types/collections/list.codon:337:13: pop from empty list
```
### Issues with the Current Approach
1. **Implicit assumption of no handler**. The first `_Unwind_RaiseException` call implicitly relies on the absence of any other codon-exception catch handler so that execution eventually reaches `seq_terminate` (the same assumption holds in the AOT path as well). This makes it significantly harder to integrate foreign exception-handling mechanisms — for instance, adding pybind11 bindings that need to intercept and translate Codon exceptions into Python exceptions.
2. **Personality routine complexity**. If we want to propagate typed exceptions (rather than just a single opaque `JITError`), we would need to implement a full personality routine. Supporting such a routine correctly across different platforms (macOS / Linux / Windows, various unwinder ABIs) introduces considerable complexity.
### Proposed Alternative: Explicit Return-Based Error Propagation
Drawing inspiration from Rust's approach, I'd like to propose that — at least for the JIT path — the Codon compiler lower `raise`, `try/except/finally`, and exception propagation into an `explicit CFG with a status-return calling convention`, eliminating the need for stack unwinding entirely.
**Conceptual Model**
Every Codon function that may raise an exception would be lowered to a `noexcept` function returning a status code:
```C++
enum SeqStatus : int32_t {
SEQ_OK = 0,
SEQ_ERROR = 1,
};
SeqStatus function(
ReturnType *result,
SeqException **error,
Arguments...
) noexcept;
```
For example, the logical Codon code:
```python
def divide(a: int, b: int) -> int:
if b == 0:
raise ZeroDivisionError()
return a // b
```
would be lowered to something equivalent to:
```C++
SeqStatus divide(int64_t *result, SeqException **error,
int64_t a, int64_t b) noexcept {
if (b == 0) {
*error = makeZeroDivisionError();
return SEQ_ERROR;
}
*result = a / b;
return SEQ_OK;
}
```
And the caller site:
```C++
SeqException *error = nullptr;
int64_t result;
if (divide(&result, &error, a, b) != SEQ_OK)
return propagate(error);
```
This is semantically identical to Rust's:
```rust
let result = divide(a, b)?;
```
### Why This Could Be Worth It
- **Simplicity**. No dependency on platform-specific unwind libraries or personality routines — just ordinary function calls and branches.
- **Portability**. The mechanism is pure C-level ABI; it works identically on every target without platform-specific unwind support.
- **Extensibility**. Foreign code (e.g., a `pybind11` bridge or others) can inspect the `SeqException*` directly and translate it into whatever exception model the host language expects, with no need to intercept or re-raise unwind exceptions.
- **Performance**. The overhead of an extra return-value check on the happy path is minimal and may, in practice, be comparable to — or even lower than — the cost of maintaining landing pads and LSDA tables required by the current unwind-based approach.
I'd love to hear the maintainers' and the community's thoughts on this. Happy to contribute to the implementation if there's interest!
Thanks for your time. 🙏
Contributor guide
Research direction
Start by reading the current JIT exception path described through seq_throw, _Unwind_RaiseException, and seq_terminate, then compare it with the proposed lowering of raise, try/except/finally, and propagation into an explicit CFG with a status-return calling convention. A useful outcome would be a maintainer decision on whether this design should be pursued and what implementation scope and compatibility requirements it entails.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100