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();