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 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 09:46:13 -05:00
parent 62be78ff99
commit c0ef71beb7
5 changed files with 198 additions and 2 deletions

View File

@@ -475,6 +475,10 @@ pub struct Interpreter {
start_time: std::time::Instant,
/// Migration registry: type_name -> (from_version -> to_version -> migration)
migrations: HashMap<String, HashMap<u32, StoredMigration>>,
/// Built-in State effect storage (uses RefCell for interior mutability)
builtin_state: RefCell<Value>,
/// Built-in Reader effect value (uses RefCell for interior mutability)
builtin_reader: RefCell<Value>,
}
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: {}.{}",

View File

@@ -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::<Vec<_>>().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};

View File

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

View File

@@ -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<a>
let a = Type::var();