diff --git a/Cargo.lock b/Cargo.lock index 98b6ab0..68719c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -776,7 +776,7 @@ dependencies = [ [[package]] name = "lux" -version = "0.1.9" +version = "0.1.11" dependencies = [ "glob", "lsp-server", diff --git a/src/codegen/c_backend.rs b/src/codegen/c_backend.rs index 86a9987..3050bdc 100644 --- a/src/codegen/c_backend.rs +++ b/src/codegen/c_backend.rs @@ -3285,6 +3285,9 @@ impl CBackend { if module_name.name == "Map" { return self.emit_map_operation(&field.name, args); } + if module_name.name == "Ref" { + return self.emit_ref_operation(&field.name, args); + } // Int module if module_name.name == "Int" && field.name == "toString" { let arg = self.emit_expr(&args[0])?; @@ -3701,6 +3704,11 @@ impl CBackend { return self.emit_map_operation(&operation.name, args); } + // Ref module + if effect.name == "Ref" { + return self.emit_ref_operation(&operation.name, args); + } + // Built-in Console effect if effect.name == "Console" { if operation.name == "print" { @@ -5185,6 +5193,42 @@ impl CBackend { } } + fn emit_ref_operation(&mut self, op: &str, args: &[Expr]) -> Result { + match op { + "new" => { + let val = self.emit_expr(&args[0])?; + let boxed = self.box_value(&val, None); + let temp = format!("_ref_new_{}", self.fresh_name()); + self.writeln(&format!("void** {} = (void**)malloc(sizeof(void*));", temp)); + self.writeln(&format!("*{} = {};", temp, boxed)); + Ok(temp) + } + "get" => { + let r = self.emit_expr(&args[0])?; + Ok(format!("(*({})) /* Ref.get */", r)) + } + "set" => { + let r = self.emit_expr(&args[0])?; + let val = self.emit_expr(&args[1])?; + let boxed = self.box_value(&val, None); + self.writeln(&format!("*{} = {};", r, boxed)); + Ok("0 /* Unit */".to_string()) + } + "update" => { + let r = self.emit_expr(&args[0])?; + let f = self.emit_expr(&args[1])?; + let temp = format!("_ref_upd_{}", self.fresh_name()); + self.writeln(&format!("void* {} = ((void*(*)(void*)){})(*({}));", temp, f, r)); + self.writeln(&format!("*{} = {};", r, temp)); + Ok("0 /* Unit */".to_string()) + } + _ => Err(CGenError { + message: format!("Unsupported Ref operation: {}", op), + span: None, + }), + } + } + fn emit_expr_with_substitution(&mut self, expr: &Expr, from: &str, to: &str) -> Result { // Simple substitution - in a real implementation, this would be more sophisticated match expr { diff --git a/src/codegen/js_backend.rs b/src/codegen/js_backend.rs index 3762e16..cb19d8f 100644 --- a/src/codegen/js_backend.rs +++ b/src/codegen/js_backend.rs @@ -1242,6 +1242,9 @@ impl JsBackend { if module_name.name == "Map" { return self.emit_map_operation(&field.name, args); } + if module_name.name == "Ref" { + return self.emit_ref_operation(&field.name, args); + } } } @@ -1388,6 +1391,11 @@ impl JsBackend { return self.emit_map_operation(&operation.name, args); } + // Special case: Ref module operations (not an effect) + if effect.name == "Ref" { + return self.emit_ref_operation(&operation.name, args); + } + // Special case: Html module operations (not an effect) if effect.name == "Html" { return self.emit_html_operation(&operation.name, args); @@ -2486,6 +2494,37 @@ impl JsBackend { } } + fn emit_ref_operation( + &mut self, + operation: &str, + args: &[Expr], + ) -> Result { + match operation { + "new" => { + let val = self.emit_expr(&args[0])?; + Ok(format!("({{value: {}}})", val)) + } + "get" => { + let r = self.emit_expr(&args[0])?; + Ok(format!("({}.value)", r)) + } + "set" => { + let r = self.emit_expr(&args[0])?; + let val = self.emit_expr(&args[1])?; + Ok(format!("({}.value = {}, undefined)", r, val)) + } + "update" => { + let r = self.emit_expr(&args[0])?; + let f = self.emit_expr(&args[1])?; + Ok(format!("({0}.value = {1}({0}.value), undefined)", r, f)) + } + _ => Err(JsGenError { + message: format!("Unknown Ref operation: {}", operation), + span: None, + }), + } + } + /// Emit Html module operations for type-safe HTML construction fn emit_html_operation( &mut self, diff --git a/src/interpreter.rs b/src/interpreter.rs index eca1d94..3f871ea 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -144,6 +144,12 @@ pub enum BuiltinFn { MapFromList, MapToList, MapMerge, + + // Ref operations + RefNew, + RefGet, + RefSet, + RefUpdate, } /// Runtime value @@ -181,6 +187,8 @@ pub enum Value { name: String, arity: usize, }, + /// Mutable reference cell + Ref(Rc>), } impl Value { @@ -203,6 +211,7 @@ impl Value { Value::Versioned { .. } => "Versioned", Value::Json(_) => "Json", Value::ExternFn { .. } => "ExternFn", + Value::Ref(_) => "Ref", } } @@ -258,6 +267,7 @@ impl Value { t1 == t2 && v1 == v2 && Value::values_equal(val1, val2) } (Value::Json(j1), Value::Json(j2)) => j1 == j2, + (Value::Ref(r1), Value::Ref(r2)) => Rc::ptr_eq(r1, r2), // Functions and handlers cannot be compared for equality _ => false, } @@ -414,6 +424,7 @@ impl fmt::Display for Value { } Value::Json(json) => write!(f, "{}", json), Value::ExternFn { name, .. } => write!(f, "", name), + Value::Ref(cell) => write!(f, "", cell.borrow()), } } } @@ -1202,6 +1213,15 @@ impl Interpreter { ("merge".to_string(), Value::Builtin(BuiltinFn::MapMerge)), ])); env.define("Map", map_module); + + // Ref module + let ref_module = Value::Record(HashMap::from([ + ("new".to_string(), Value::Builtin(BuiltinFn::RefNew)), + ("get".to_string(), Value::Builtin(BuiltinFn::RefGet)), + ("set".to_string(), Value::Builtin(BuiltinFn::RefSet)), + ("update".to_string(), Value::Builtin(BuiltinFn::RefUpdate)), + ])); + env.define("Ref", ref_module); } /// Execute a program @@ -3440,6 +3460,56 @@ impl Interpreter { } Ok(EvalResult::Value(Value::Map(map1))) } + + BuiltinFn::RefNew => { + if args.len() != 1 { + return Err(err("Ref.new requires 1 argument")); + } + Ok(EvalResult::Value(Value::Ref(Rc::new(RefCell::new(args.into_iter().next().unwrap()))))) + } + + BuiltinFn::RefGet => { + if args.len() != 1 { + return Err(err("Ref.get requires 1 argument")); + } + match &args[0] { + Value::Ref(cell) => Ok(EvalResult::Value(cell.borrow().clone())), + v => Err(err(&format!("Ref.get expects Ref, got {}", v.type_name()))), + } + } + + BuiltinFn::RefSet => { + if args.len() != 2 { + return Err(err("Ref.set requires 2 arguments: ref, value")); + } + match &args[0] { + Value::Ref(cell) => { + *cell.borrow_mut() = args[1].clone(); + Ok(EvalResult::Value(Value::Unit)) + } + v => Err(err(&format!("Ref.set expects Ref as first argument, got {}", v.type_name()))), + } + } + + BuiltinFn::RefUpdate => { + if args.len() != 2 { + return Err(err("Ref.update requires 2 arguments: ref, fn")); + } + match &args[0] { + Value::Ref(cell) => { + let old = cell.borrow().clone(); + let result = self.eval_call(args[1].clone(), vec![old], span)?; + match result { + EvalResult::Value(new_val) => { + *cell.borrow_mut() = new_val; + } + _ => return Err(err("Ref.update callback must return a value")), + } + Ok(EvalResult::Value(Value::Unit)) + } + v => Err(err(&format!("Ref.update expects Ref as first argument, got {}", v.type_name()))), + } + } } } diff --git a/src/main.rs b/src/main.rs index aff62fa..7fc4e02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5855,6 +5855,58 @@ c")"#; assert_eq!(eval(source).unwrap(), "Some(30)"); } + // Ref cell tests + #[test] + fn test_ref_new_and_get() { + let source = r#" + let r = Ref.new(42) + let result = Ref.get(r) + "#; + assert_eq!(eval(source).unwrap(), "42"); + } + + #[test] + fn test_ref_set() { + let source = r#" + let r = Ref.new(0) + let _ = Ref.set(r, 10) + let result = Ref.get(r) + "#; + assert_eq!(eval(source).unwrap(), "10"); + } + + #[test] + fn test_ref_update() { + let source = r#" + let r = Ref.new(5) + let _ = Ref.update(r, fn(n) => n + 1) + let result = Ref.get(r) + "#; + assert_eq!(eval(source).unwrap(), "6"); + } + + #[test] + fn test_ref_multiple_updates() { + let source = r#" + let counter = Ref.new(0) + let _ = Ref.set(counter, 1) + let _ = Ref.update(counter, fn(n) => n * 10) + let _ = Ref.set(counter, Ref.get(counter) + 5) + let result = Ref.get(counter) + "#; + assert_eq!(eval(source).unwrap(), "15"); + } + + #[test] + fn test_ref_with_string() { + let source = r#" + let r = Ref.new("hello") + let _ = Ref.set(r, "world") + let result = Ref.get(r) + "#; + assert_eq!(eval(source).unwrap(), "\"world\""); + } + #[test] fn test_file_copy() { use std::io::Write; diff --git a/src/typechecker.rs b/src/typechecker.rs index 5c59d22..0df52ef 100644 --- a/src/typechecker.rs +++ b/src/typechecker.rs @@ -3026,6 +3026,9 @@ impl TypeChecker { "Map" if resolved_args.len() == 2 => { return Type::Map(Box::new(resolved_args[0].clone()), Box::new(resolved_args[1].clone())); } + "Ref" if resolved_args.len() == 1 => { + return Type::Ref(Box::new(resolved_args[0].clone())); + } _ => {} } } diff --git a/src/types.rs b/src/types.rs index f5af920..d7aafb6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -49,6 +49,8 @@ pub enum Type { Option(Box), /// Map type (sugar for App(Map, [K, V])) Map(Box, Box), + /// Ref type — mutable reference cell holding a value of type T + Ref(Box), /// Versioned type (e.g., User @v2) Versioned { base: Box, @@ -120,7 +122,7 @@ impl Type { } Type::Tuple(elements) => elements.iter().any(|e| e.contains_var(var)), Type::Record(fields) => fields.iter().any(|(_, t)| t.contains_var(var)), - Type::List(inner) | Type::Option(inner) => inner.contains_var(var), + Type::List(inner) | Type::Option(inner) | Type::Ref(inner) => inner.contains_var(var), Type::Map(k, v) => k.contains_var(var) || v.contains_var(var), Type::Versioned { base, .. } => base.contains_var(var), _ => false, @@ -161,6 +163,7 @@ impl Type { ), Type::List(inner) => Type::List(Box::new(inner.apply(subst))), Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))), + Type::Ref(inner) => Type::Ref(Box::new(inner.apply(subst))), Type::Map(k, v) => Type::Map(Box::new(k.apply(subst)), Box::new(v.apply(subst))), Type::Versioned { base, version } => Type::Versioned { base: Box::new(base.apply(subst)), @@ -211,7 +214,7 @@ impl Type { } vars } - Type::List(inner) | Type::Option(inner) => inner.free_vars(), + Type::List(inner) | Type::Option(inner) | Type::Ref(inner) => inner.free_vars(), Type::Map(k, v) => { let mut vars = k.free_vars(); vars.extend(v.free_vars()); @@ -288,6 +291,7 @@ impl fmt::Display for Type { } Type::List(inner) => write!(f, "List<{}>", inner), Type::Option(inner) => write!(f, "Option<{}>", inner), + Type::Ref(inner) => write!(f, "Ref<{}>", inner), Type::Map(k, v) => write!(f, "Map<{}, {}>", k, v), Type::Versioned { base, version } => { write!(f, "{} {}", base, version) @@ -1946,6 +1950,32 @@ impl TypeEnv { ]); env.bind("Map", TypeScheme::mono(map_module_type)); + // Ref module + let ref_inner = || Type::var(); + let ref_type = || Type::Ref(Box::new(Type::var())); + let ref_module_type = Type::Record(vec![ + ( + "new".to_string(), + Type::function(vec![ref_inner()], ref_type()), + ), + ( + "get".to_string(), + Type::function(vec![ref_type()], ref_inner()), + ), + ( + "set".to_string(), + Type::function(vec![ref_type(), ref_inner()], Type::Unit), + ), + ( + "update".to_string(), + Type::function( + vec![ref_type(), Type::function(vec![ref_inner()], ref_inner())], + Type::Unit, + ), + ), + ]); + env.bind("Ref", TypeScheme::mono(ref_module_type)); + // Result module let result_type = Type::App { constructor: Box::new(Type::Named("Result".to_string())), @@ -2185,6 +2215,9 @@ impl TypeEnv { Type::Map(k, v) => { Type::Map(Box::new(self.expand_type_alias(k)), Box::new(self.expand_type_alias(v))) } + Type::Ref(inner) => { + Type::Ref(Box::new(self.expand_type_alias(inner))) + } Type::Versioned { base, version } => { Type::Versioned { base: Box::new(self.expand_type_alias(base)), @@ -2345,6 +2378,9 @@ pub fn unify(t1: &Type, t2: &Type) -> Result { // Option (Type::Option(a), Type::Option(b)) => unify(a, b), + // Ref + (Type::Ref(a), Type::Ref(b)) => unify(a, b), + // Map (Type::Map(k1, v1), Type::Map(k2, v2)) => { let s1 = unify(k1, k2)?;