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:
45
examples/handlers.lux
Normal file
45
examples/handlers.lux
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Demonstrating resumable effect handlers in Lux
|
||||||
|
//
|
||||||
|
// Handlers can use `resume(value)` to return a value to the effect call site
|
||||||
|
// and continue the computation. This enables powerful control flow patterns.
|
||||||
|
//
|
||||||
|
// Expected output:
|
||||||
|
// [INFO] Starting computation
|
||||||
|
// [DEBUG] Intermediate result: 10
|
||||||
|
// [INFO] Computation complete
|
||||||
|
// Final result: 20
|
||||||
|
|
||||||
|
// Define a custom logging effect
|
||||||
|
effect Logger {
|
||||||
|
fn log(level: String, msg: String): Unit
|
||||||
|
fn getLogLevel(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
// A function that uses the Logger effect
|
||||||
|
fn compute(): Int with {Logger} = {
|
||||||
|
Logger.log("INFO", "Starting computation")
|
||||||
|
let x = 10
|
||||||
|
Logger.log("DEBUG", "Intermediate result: " + toString(x))
|
||||||
|
let result = x * 2
|
||||||
|
Logger.log("INFO", "Computation complete")
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// A handler that prints logs with brackets and resumes with Unit
|
||||||
|
handler prettyLogger: Logger {
|
||||||
|
fn log(level, msg) = {
|
||||||
|
Console.print("[" + level + "] " + msg)
|
||||||
|
resume(())
|
||||||
|
}
|
||||||
|
fn getLogLevel() = resume("DEBUG")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function
|
||||||
|
fn main(): Unit with {Console} = {
|
||||||
|
let result = run compute() with {
|
||||||
|
Logger = prettyLogger
|
||||||
|
}
|
||||||
|
Console.print("Final result: " + toString(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = run main() with {}
|
||||||
@@ -434,6 +434,8 @@ pub enum EvalResult {
|
|||||||
args: Vec<Value>,
|
args: Vec<Value>,
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
/// Resume from a handler - the value becomes the effect operation's return value
|
||||||
|
Resume(Value),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Effect trace entry for debugging
|
/// Effect trace entry for debugging
|
||||||
@@ -480,6 +482,8 @@ pub struct Interpreter {
|
|||||||
builtin_state: RefCell<Value>,
|
builtin_state: RefCell<Value>,
|
||||||
/// Built-in Reader effect value (uses RefCell for interior mutability)
|
/// Built-in Reader effect value (uses RefCell for interior mutability)
|
||||||
builtin_reader: RefCell<Value>,
|
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 {
|
impl Interpreter {
|
||||||
@@ -499,6 +503,7 @@ impl Interpreter {
|
|||||||
migrations: HashMap::new(),
|
migrations: HashMap::new(),
|
||||||
builtin_state: RefCell::new(Value::Unit),
|
builtin_state: RefCell::new(Value::Unit),
|
||||||
builtin_reader: 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
|
// Continue the tail call without growing the stack
|
||||||
result = self.eval_call(func, args, span)?;
|
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,
|
span,
|
||||||
} => self.eval_run(expr, handlers, env, *span),
|
} => self.eval_run(expr, handlers, env, *span),
|
||||||
|
|
||||||
Expr::Resume { value, span } => Err(RuntimeError {
|
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(),
|
message: "Resume called outside of handler".to_string(),
|
||||||
span: Some(*span),
|
span: Some(*span),
|
||||||
}),
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1393,6 +1410,7 @@ impl Interpreter {
|
|||||||
EvalResult::TailCall { func, args, span } => {
|
EvalResult::TailCall { func, args, span } => {
|
||||||
result = self.eval_call(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());
|
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 {
|
} else {
|
||||||
// No handler found - check for built-in effects
|
// No handler found - check for built-in effects
|
||||||
self.handle_builtin_effect(&request)
|
self.handle_builtin_effect(&request)
|
||||||
|
|||||||
41
src/main.rs
41
src/main.rs
@@ -1292,6 +1292,47 @@ c")"#;
|
|||||||
// Timestamp should be a reasonable Unix time in milliseconds (after 2020)
|
// Timestamp should be a reasonable Unix time in milliseconds (after 2020)
|
||||||
assert!(timestamp > 1577836800000, "Timestamp should be after 2020");
|
assert!(timestamp > 1577836800000, "Timestamp should be after 2020");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resumable_handler() {
|
||||||
|
// Test that resume works in handler bodies
|
||||||
|
let source = r#"
|
||||||
|
effect Counter {
|
||||||
|
fn increment(): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
fn useCounter(): Int with {Counter} = {
|
||||||
|
let a = Counter.increment()
|
||||||
|
let b = Counter.increment()
|
||||||
|
a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
handler countingHandler: Counter {
|
||||||
|
fn increment() = resume(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = run useCounter() with {
|
||||||
|
Counter = countingHandler
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
||||||
|
// Each increment returns 10, so a + b = 10 + 10 = 20
|
||||||
|
assert_eq!(result, "20");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resume_outside_handler_fails() {
|
||||||
|
// Resume outside handler should fail at runtime
|
||||||
|
let source = r#"
|
||||||
|
fn bad(): Int = resume(42)
|
||||||
|
let result = bad()
|
||||||
|
"#;
|
||||||
|
let result = run_with_effects(source, Value::Unit, Value::Unit);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err();
|
||||||
|
assert!(err_msg.contains("outside") || err_msg.contains("Resume"),
|
||||||
|
"Error should mention resume outside handler: {}", err_msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostic rendering tests
|
// Diagnostic rendering tests
|
||||||
|
|||||||
Reference in New Issue
Block a user