exaloop / exaloop/codon

Exception handing in JIT: unwind-based vs. return-based approach?

Open
#855 3 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.