feat: implement resumable effect handlers
Add support for the `resume(value)` expression in effect handler
bodies. When resume is called, the value becomes the return value
of the effect operation, allowing handlers to provide values back
to the calling code.
Implementation:
- Add Resume(Value) variant to EvalResult
- Add in_handler_depth tracking to Interpreter
- Update Expr::Resume evaluation to return Resume when in handler
- Handle Resume results in handle_effect to use as return value
- Add 2 tests for resumable handlers
Example usage:
```lux
handler prettyLogger: Logger {
fn log(level, msg) = {
Console.print("[" + level + "] " + msg)
resume(()) // Return Unit to the call site
}
}
```
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -434,6 +434,8 @@ pub enum EvalResult {
|
||||
args: Vec<Value>,
|
||||
span: Span,
|
||||
},
|
||||
/// Resume from a handler - the value becomes the effect operation's return value
|
||||
Resume(Value),
|
||||
}
|
||||
|
||||
/// Effect trace entry for debugging
|
||||
@@ -480,6 +482,8 @@ pub struct Interpreter {
|
||||
builtin_state: RefCell<Value>,
|
||||
/// Built-in Reader effect value (uses RefCell for interior mutability)
|
||||
builtin_reader: RefCell<Value>,
|
||||
/// Depth of handler context (> 0 means we're inside a handler body where resume is valid)
|
||||
in_handler_depth: usize,
|
||||
}
|
||||
|
||||
impl Interpreter {
|
||||
@@ -499,6 +503,7 @@ impl Interpreter {
|
||||
migrations: HashMap::new(),
|
||||
builtin_state: RefCell::new(Value::Unit),
|
||||
builtin_reader: RefCell::new(Value::Unit),
|
||||
in_handler_depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -945,6 +950,10 @@ impl Interpreter {
|
||||
// Continue the tail call without growing the stack
|
||||
result = self.eval_call(func, args, span)?;
|
||||
}
|
||||
EvalResult::Resume(v) => {
|
||||
// Resume propagates up - return the value
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1165,10 +1174,18 @@ impl Interpreter {
|
||||
span,
|
||||
} => self.eval_run(expr, handlers, env, *span),
|
||||
|
||||
Expr::Resume { value, span } => Err(RuntimeError {
|
||||
message: "Resume called outside of handler".to_string(),
|
||||
span: Some(*span),
|
||||
}),
|
||||
Expr::Resume { value, span } => {
|
||||
if self.in_handler_depth > 0 {
|
||||
// We're inside a handler body - evaluate the value and return Resume
|
||||
let val = self.eval_expr(value, env)?;
|
||||
Ok(EvalResult::Resume(val))
|
||||
} else {
|
||||
Err(RuntimeError {
|
||||
message: "Resume called outside of handler".to_string(),
|
||||
span: Some(*span),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1393,6 +1410,7 @@ impl Interpreter {
|
||||
EvalResult::TailCall { func, args, span } => {
|
||||
result = self.eval_call(func, args, span)?;
|
||||
}
|
||||
EvalResult::Resume(v) => return Ok(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2107,7 +2125,25 @@ impl Interpreter {
|
||||
env.define(¶m.name, request.args[i].clone());
|
||||
}
|
||||
}
|
||||
self.eval_expr(&body, &env)
|
||||
// Enter handler context (enables resume)
|
||||
self.in_handler_depth += 1;
|
||||
let eval_result = self.eval_expr_inner(&body, &env);
|
||||
self.in_handler_depth -= 1;
|
||||
|
||||
// Handle Resume result - use the resumed value as the effect's return value
|
||||
match eval_result {
|
||||
Ok(EvalResult::Resume(value)) => Ok(value),
|
||||
Ok(EvalResult::Value(value)) => Ok(value),
|
||||
Ok(EvalResult::Effect(req)) => {
|
||||
// Handler body can perform effects - handle them
|
||||
self.handle_effect(req)
|
||||
}
|
||||
Ok(EvalResult::TailCall { func, args, span }) => {
|
||||
// Tail call in handler - evaluate it
|
||||
self.eval_call_to_value(func, args, span)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
// No handler found - check for built-in effects
|
||||
self.handle_builtin_effect(&request)
|
||||
|
||||
Reference in New Issue
Block a user