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:
2026-02-13 09:54:51 -05:00
parent 52ad5f8781
commit 9511957076
3 changed files with 127 additions and 5 deletions

View File

@@ -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(&param.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)

View File

@@ -1292,6 +1292,47 @@ c")"#;
// Timestamp should be a reasonable Unix time in milliseconds (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