When hew eval JIT-compiles your code, it runs it in the same process as the REPL. That’s what makes it fast — no fork, no IPC, no round-trip to a worker. It also means that when the generated code does something the OS doesn’t approve of — a null dereference, a bad FFI call, an out-of-bounds write — the crash doesn’t just take down your program. It takes down the REPL.
No recovery, no diagnostic — the session is just gone.
For a language still finding its edge cases, that’s a real problem. You’d hit a bug in the generated code, lose the session, restart, and have no record of what happened.
Catching panics
Rust’s catch_unwind can intercept Rust panics before they unwind the whole process. Wrapping the LLJIT invocation in one means that a panic in generated code — or in the machinery driving it — gives the REPL a recoverable error instead of death:
let exit_code = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
unsafe { hew_jit_session_eval_msgpack(guard.session, msgpack_data) }
}))
.map_err(|_| JitError::PanicCaught)?;If it panics, the REPL gets JitError::PanicCaught, renders an error message, and returns to the prompt. The session continues.
AssertUnwindSafe is warranted here: guard is the sole owner of the session pointer (Drop calls destroy exactly once), exit_code is a plain i64, and msgpack_data is a shared reference — none of them leave the JIT in a bad state across an unwind.
Scope of the current protection: catch_unwind only activates when Rust’s panic strategy is panic = "unwind". Production builds use panic = "abort", so in production a panic still terminates the process. (The wrapper is the right seam for when that changes — or when a worker-process JIT path proves out — but right now it catches panics in test builds and staging, which is where you find them before they ship.)
When the OS is the one crashing you
Signals are a different matter. SIGSEGV and SIGABRT can’t be caught with catch_unwind — the OS delivers the signal and the process is gone before Rust can do anything about it. There’s no in-process recovery path.
What we can do is notice it happened.
At REPL startup, a clean-exit marker is written to the platform state directory. At clean exit — :quit or readline EOF — the marker is deleted. If the marker exists at startup, the previous session ended abruptly. A separate timestamp log, pruned to seven days, tracks how many times this has occurred:
warning: last session ended in an uncaught signal (N such events in the last 7 days)This doesn’t fix the crash. It makes it visible. When the count is 1, it might be a fluke. When it’s 8 in a week, something is reliably wrong — and now you have a number to point at.
The longer-term plan is automatic demotion to a worker-process JIT path (separate process, crashes don’t reach the REPL host) when the crash count exceeds a threshold. The marker and counter infrastructure is already wired; the demotion logic is what’s deferred. (It’s in the backlog under “jit-worker-fallback” if you’re looking for it.)
Per-cell LLJIT instances
Each REPL submission gets a fresh llvm::orc::LLJIT instance with no cross-cell symbol sharing. The previous design tried to accumulate a persistent JIT session and share symbols between submissions. That created lifetime and ownership complexity that made crashes harder to isolate — a buggy cell could corrupt the state of a previous one.
Fresh instance per cell is simpler. A crashing submission can’t touch the state from an earlier one.
The trade-off is startup cost. A fresh LLJIT isn’t free, and you feel it slightly. For an interactive REPL where there’s a human typing between submissions, the latency is acceptable. For batch evaluation — where you’d want to amortise JIT setup across many cells — the AOT+spawn path (--jit=worker) is the right route anyway.
GDB and perf now work
Also in this window: GDB and perf debug listeners got registered (edb5605f), via createGDBRegistrationListener and createPerfJITEventListener.
Before this, setting a breakpoint in hew eval-compiled code was technically possible but practically useless — the debugger couldn’t resolve any symbols. JIT-compiled frames showed up as ??? in backtraces. Now they show up with correct symbol names. You can step through hew eval output in lldb and it actually tells you where you are.