From 9511957076f7edc0d62bd8f5ddf62edb701043be Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 13 Feb 2026 09:54:51 -0500 Subject: [PATCH] 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 --- examples/handlers.lux | 45 ++++++++++++++++++++++++++++++++++++++++++ src/interpreter.rs | 46 ++++++++++++++++++++++++++++++++++++++----- src/main.rs | 41 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 examples/handlers.lux diff --git a/examples/handlers.lux b/examples/handlers.lux new file mode 100644 index 0000000..986dd87 --- /dev/null +++ b/examples/handlers.lux @@ -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 {} diff --git a/src/interpreter.rs b/src/interpreter.rs index 492dd02..9e218e0 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -434,6 +434,8 @@ pub enum EvalResult { args: Vec, 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, /// Built-in Reader effect value (uses RefCell for interior mutability) builtin_reader: RefCell, + /// 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) diff --git a/src/main.rs b/src/main.rs index b7e800a..876cf75 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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