Interrupts (resumable conditions)
Status: Implemented —
skip+recover, for top-level functions,entry, and class methods.retry, value-producing signals, and checking of theinterrupts Tannotation (it is currently parsed but not enforced) remain future work (see Limitations). Runnable:examples/interrupt_demo.xi.
Summary
interrupt introduces resumable conditions to Xi. When a function signals
an interrupt, that function is suspended at the signal site — it does not
unwind. A handler in an enclosing try/catch runs while the suspended frame is
still alive and decides what happens next:
recover— the suspended function resumes: it runs the inlinerecover { }block at the signal site and continues from there.skip— the suspended function is abandoned; control returns after thetry.
It is checked: a function that may signal declares interrupts T, and the
compiler verifies that callers either handle or re-declare it.
This is the Common Lisp condition/restart model (also seen in Smalltalk's resumable exceptions and algebraic effects), adapted to Xi's C backend and its pure/impure function-kind system.
Execution model (the key idea)
The running method that raises an interrupt gets interrupted, and only continues once the interruption is managed at an upper stack frame.
foo() running ──signal T──▶ foo SUSPENDED here (frame kept alive)
│
▼
handler search up the stack → matching catch
│
catch body runs (no unwinding yet), decides:
├─ recover ─▶ run foo's recover{} block, foo CONTINUES
└─ skip ────▶ unwind to the try, foo ABANDONED
Contrast with Xi's existing Result and with classic exceptions:
| Mechanism | Use | Stack behaviour |
|---|---|---|
Result (T!, ?) | expected, local errors handled as values | none; caller inspects |
| Interrupt | recoverable conditions; an outer policy decides | handler runs with the signalling frame intact, then resumes or abandons |
| (classic exceptions) | — | would unwind before the handler; not in Xi |
Syntax
// 1. Declare an interrupt type (a condition with a payload).
interrupt FooCalcInt { x: Integer }
// 2. A function that may raise it declares `interrupts`. `signal` raises it;
// the `recover { }` block is the inline restart — it runs only if a handler
// chooses to recover, in this (suspended) function's own frame.
consumer foo(n: Integer) interrupts FooCalcInt {
if n > 20 {
signal FooCalcInt { x: n } recover {
system.stdout.writeln("recovering; clamped " + n)
}
}
system.stdout.writeln("foo continues normally") // reached after recover
}
// 3. Handle it. The catch body DECIDES; it does not contain the recovery code.
try {
foo(24)
} catch e: FooCalcInt {
if e.x > 100 { skip } // abandon the rest of foo; resume after `try`
else { recover } // resume foo: run its recover{} block, continue
}
Grammar (sketch)
interrupt_decl ::= "interrupt" Ident "{" field ("," field)* "}"
signal_stmt ::= "signal" Type "{" fields "}" "recover" block
func_decl ::= ... ("interrupts" Type ("," Type)*)? block
try_stmt ::= "try" block ("catch" Ident ":" Type block)+
resolution ::= "skip" | "recover"
catch is paren-free (like if/for); the payload uses the compound-type
literal { field: value }.
Semantics
Resolutions
recover— run therecover { }block at the signal site, then continue the signalling function at the statement after thesignal. The recovery logic lives with the code that knows how to recover; the handler only opts in.skip— unwind from the signal site back to thetry; the signalling function's remaining work is abandoned. Control resumes after thetry/catch.retry— deferred (not in the first version): would re-execute the interrupting operation.
A catch body must select exactly one resolution (skip or recover) on every
path. It may run other statements first (e.g. logging) — subject to the
restriction below.
Handler lookup
signal T searches the dynamically-enclosing handler stack for the nearest
try whose catch matches T (by type). The matching handler runs without
unwinding and returns a resolution, which the signal site then enacts. If no
handler matches, the signal is unhandled (see checked signatures).
The interrupts annotation
- A function that may signal declares
interrupts T(multiple:interrupts A, B). - An interrupt that reaches
mainwith no matching handler is a panic (xc: unhandled interrupt: T).
The annotation is currently parsed but not enforced — the compiler does not yet verify that every
signalsite is declared, nor that callers handle or re-declare. That effect-checking pass is future work; todayinterrupts Tis documentation that the runtime backs up with the unhandled-panic.
Purity
Signalling is an effect, permitted only in the impure kinds — consumer,
producer, creator (and entry). The pure kinds — mapper, projector,
predicate, reducer — may neither signal nor call an interrupts function.
This reuses Xi's existing purity line.
The catch-as-function restriction
To run a handler before unwinding (so recover can resume the suspended
frame), the catch body is compiled as a function over the payload. A catch
body may therefore read:
- the interrupt payload (
e), - module-level / global state,
- and call functions,
but may not capture try-scope local variables (Xi has no closures). This is
the simplification that makes resumption implementable without continuations or
coroutines. (Future work could lift it with explicit captures.)
Worked example
interrupt RateLimited { retryAfter: Integer }
producer fetch(url: String) interrupts RateLimited {
let r = http.get(url)?
if r.status == 429 {
signal RateLimited { retryAfter: 5 } recover {
system.stdout.writeln("backing off, then continuing")
}
}
// ... use r ...
}
consumer run() {
try {
fetch("http://example.com/")
} catch e: RateLimited {
system.stdout.writeln("rate limited; retryAfter=" + e.retryAfter)
recover // resume fetch: it runs its backoff and continues
}
}
Static checks / diagnostics
signal Tin a function not declaringinterrupts T→ error (suggest adding it or wrapping intry).- Calling an
interrupts Tfunction without handling or re-declaringT→ error. signal/ calling aninterruptsfunction from a pure kind → error.catchbody capturing atry-local → error (restriction explained).catchbody that does not select a resolution on all paths → error.
Limitations & future work
retryresolution (re-execute the interrupting operation).- Value-producing signals (
use-value):let y = signal T {..} recover {..}where the recover block yields the value of the signal expression. - Value-producing
try(the try block yielding a result; today a statement). - Multiple named restarts at one site (beyond a single
recover). - Closures in
catchto lift the local-capture restriction. - Interaction with
async.
Decisions on record
- Raise with
signal; declare capability withinterrupts T; type keyword isinterrupt. - Resolutions are
skip(abandon) andrecover(resume);retrydeferred. - Recovery code is an inline
recover { }restart at the signal site; the handler only chooses. - Execution model: the signalling function suspends and resumes only after an enclosing handler decides.
- Interrupts are checked; pure kinds may not signal.
- First version implements
skip+recover. Ship this proposal first.