From c0ef71beb7c4e140617f21a7d2fdc4abe1915b48 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 13 Feb 2026 09:46:13 -0500 Subject: [PATCH] feat: implement built-in State, Reader, and Fail effects Add runtime support for the State, Reader, and Fail effects that were already defined in the type system. These effects can now be used in effectful code blocks. Changes: - Add builtin_state and builtin_reader fields to Interpreter - Implement State.get and State.put in handle_builtin_effect - Implement Reader.ask in handle_builtin_effect - Add Reader effect definition to types.rs - Add Reader to built-in effects list in typechecker - Add set_state/get_state/set_reader/get_reader methods - Add 6 new tests for built-in effects - Add examples/builtin_effects.lux demonstrating usage Co-Authored-By: Claude Opus 4.5 --- examples/builtin_effects.lux | 45 +++++++++++++++++ src/interpreter.rs | 43 +++++++++++++++++ src/main.rs | 94 ++++++++++++++++++++++++++++++++++++ src/typechecker.rs | 4 +- src/types.rs | 14 ++++++ 5 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 examples/builtin_effects.lux diff --git a/examples/builtin_effects.lux b/examples/builtin_effects.lux new file mode 100644 index 0000000..2cb38cf --- /dev/null +++ b/examples/builtin_effects.lux @@ -0,0 +1,45 @@ +// Demonstrating built-in effects in Lux +// +// Lux provides several built-in effects: +// - Console: print and read from terminal +// - Fail: early termination with error +// - State: get/put mutable state (requires runtime initialization) +// - Reader: read-only environment access (requires runtime initialization) +// +// This example demonstrates Console and Fail effects. +// +// Expected output: +// Starting computation... +// Step 1: validating input +// Step 2: processing +// Result: 42 +// Done! + +// A function that can fail +fn safeDivide(a: Int, b: Int): Int with {Fail} = + if b == 0 then Fail.fail("Division by zero") + else a / b + +// A function that validates input +fn validatePositive(n: Int): Int with {Fail} = + if n < 0 then Fail.fail("Negative number not allowed") + else n + +// A computation that uses multiple effects +fn compute(input: Int): Int with {Console, Fail} = { + Console.print("Starting computation...") + Console.print("Step 1: validating input") + let validated = validatePositive(input) + Console.print("Step 2: processing") + let result = safeDivide(validated * 2, 1) + Console.print("Result: " + toString(result)) + result +} + +// Main function +fn main(): Unit with {Console} = { + let result = run compute(21) with {} + Console.print("Done!") +} + +let output = run main() with {} diff --git a/src/interpreter.rs b/src/interpreter.rs index 28adbda..e1173f0 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -475,6 +475,10 @@ pub struct Interpreter { start_time: std::time::Instant, /// Migration registry: type_name -> (from_version -> to_version -> migration) migrations: HashMap>, + /// Built-in State effect storage (uses RefCell for interior mutability) + builtin_state: RefCell, + /// Built-in Reader effect value (uses RefCell for interior mutability) + builtin_reader: RefCell, } impl Interpreter { @@ -492,9 +496,31 @@ impl Interpreter { effect_traces: Vec::new(), start_time: std::time::Instant::now(), migrations: HashMap::new(), + builtin_state: RefCell::new(Value::Unit), + builtin_reader: RefCell::new(Value::Unit), } } + /// Set the initial value for the built-in State effect + pub fn set_state(&self, value: Value) { + *self.builtin_state.borrow_mut() = value; + } + + /// Get the current value of the built-in State effect + pub fn get_state(&self) -> Value { + self.builtin_state.borrow().clone() + } + + /// Set the value for the built-in Reader effect + pub fn set_reader(&self, value: Value) { + *self.builtin_reader.borrow_mut() = value; + } + + /// Get the current value of the built-in Reader effect + pub fn get_reader(&self) -> Value { + self.builtin_reader.borrow().clone() + } + /// Enable effect tracing for debugging pub fn enable_tracing(&mut self) { self.trace_effects = true; @@ -2134,6 +2160,23 @@ impl Interpreter { span: None, }) } + ("State", "get") => { + Ok(self.builtin_state.borrow().clone()) + } + ("State", "put") => { + if let Some(value) = request.args.first() { + *self.builtin_state.borrow_mut() = value.clone(); + Ok(Value::Unit) + } else { + Err(RuntimeError { + message: "State.put requires a value argument".to_string(), + span: None, + }) + } + } + ("Reader", "ask") => { + Ok(self.builtin_reader.borrow().clone()) + } _ => Err(RuntimeError { message: format!( "Unhandled effect operation: {}.{}", diff --git a/src/main.rs b/src/main.rs index 274ce1b..ca029db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1156,6 +1156,100 @@ c")"#; assert!(result.unwrap_err().contains("pure but has effects")); } + // Built-in effect tests + mod effect_tests { + use crate::interpreter::{Interpreter, Value}; + use crate::parser::Parser; + use crate::typechecker::TypeChecker; + + fn run_with_effects(source: &str, initial_state: Value, reader_value: Value) -> Result<(String, String), String> { + let program = Parser::parse_source(source).map_err(|e| e.to_string())?; + let mut checker = TypeChecker::new(); + checker.check_program(&program).map_err(|errors| { + errors.iter().map(|e| e.to_string()).collect::>().join("\n") + })?; + let mut interp = Interpreter::new(); + interp.set_state(initial_state); + interp.set_reader(reader_value); + let result = interp.run(&program).map_err(|e| e.to_string())?; + let final_state = interp.get_state(); + Ok((format!("{}", result), format!("{}", final_state))) + } + + #[test] + fn test_state_get() { + let source = r#" + fn getValue(): Int with {State} = State.get() + let result = run getValue() with {} + "#; + let (result, _) = run_with_effects(source, Value::Int(42), Value::Unit).unwrap(); + assert_eq!(result, "42"); + } + + #[test] + fn test_state_put() { + let source = r#" + fn setValue(x: Int): Unit with {State} = State.put(x) + let result = run setValue(100) with {} + "#; + let (_, final_state) = run_with_effects(source, Value::Int(0), Value::Unit).unwrap(); + assert_eq!(final_state, "100"); + } + + #[test] + fn test_state_get_and_put() { + let source = r#" + fn increment(): Int with {State} = { + let current = State.get() + State.put(current + 1) + State.get() + } + let result = run increment() with {} + "#; + let (result, final_state) = run_with_effects(source, Value::Int(10), Value::Unit).unwrap(); + assert_eq!(result, "11"); + assert_eq!(final_state, "11"); + } + + #[test] + fn test_reader_ask() { + let source = r#" + fn getConfig(): String with {Reader} = Reader.ask() + let result = run getConfig() with {} + "#; + let (result, _) = run_with_effects(source, Value::Unit, Value::String("config_value".to_string())).unwrap(); + // Value's Display includes quotes for strings + assert_eq!(result, "\"config_value\""); + } + + #[test] + fn test_fail_effect() { + let source = r#" + fn failing(): Int with {Fail} = Fail.fail("oops") + let result = run failing() with {} + "#; + let result = run_with_effects(source, Value::Unit, Value::Unit); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("oops")); + } + + #[test] + fn test_combined_effects() { + let source = r#" + fn compute(): Int with {State, Reader} = { + let config = Reader.ask() + let current = State.get() + State.put(current + 1) + current + } + let result = run compute() with {} + "#; + let (result, final_state) = run_with_effects(source, Value::Int(5), Value::String("test".to_string())).unwrap(); + assert_eq!(result, "5"); + assert_eq!(final_state, "6"); + } + } + // Diagnostic rendering tests mod diagnostic_tests { use crate::diagnostics::{render_diagnostic_plain, Diagnostic, Severity}; diff --git a/src/typechecker.rs b/src/typechecker.rs index 3cb5932..b2f1fdc 100644 --- a/src/typechecker.rs +++ b/src/typechecker.rs @@ -935,7 +935,7 @@ impl TypeChecker { } // Built-in effects are always available - let builtin_effects = ["Console", "Fail", "State"]; + let builtin_effects = ["Console", "Fail", "State", "Reader"]; let is_builtin = builtin_effects.contains(&effect.name.as_str()); // Track this effect for inference @@ -1440,7 +1440,7 @@ impl TypeChecker { // Built-in effects are always available in run blocks (they have runtime implementations) let builtin_effects: EffectSet = - EffectSet::from_iter(["Console", "Fail", "State"].iter().map(|s| s.to_string())); + EffectSet::from_iter(["Console", "Fail", "State", "Reader"].iter().map(|s| s.to_string())); // Extend current effects with handled ones and built-in effects let combined = self.current_effects.union(&handled_effects).union(&builtin_effects); diff --git a/src/types.rs b/src/types.rs index 76e6744..26fe01a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -788,6 +788,20 @@ impl TypeEnv { }, ); + // Add Reader effect + env.effects.insert( + "Reader".to_string(), + EffectDef { + name: "Reader".to_string(), + type_params: vec!["R".to_string()], + operations: vec![EffectOpDef { + name: "ask".to_string(), + params: Vec::new(), + return_type: Type::Var(0), // R + }], + }, + ); + // Add Some and Ok, Err constructors // Some : fn(a) -> Option let a = Type::var();