diff --git a/src/interpreter.rs b/src/interpreter.rs index 0f2722d..3a2f887 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -58,6 +58,31 @@ pub enum BuiltinFn { Versioned, // Create versioned value: versioned("TypeName", 1, value) Migrate, // Migrate to version: migrate(versionedValue, targetVersion) GetVersion, // Get version number: getVersion(versionedValue) + + // Math operations + MathAbs, + MathMin, + MathMax, + MathSqrt, + MathPow, + MathFloor, + MathCeil, + MathRound, + + // Additional List operations + ListIsEmpty, + ListFind, + ListAny, + ListAll, + ListTake, + ListDrop, + + // Additional String operations + StringStartsWith, + StringEndsWith, + StringToUpper, + StringToLower, + StringSubstring, } /// Runtime value @@ -708,6 +733,15 @@ impl Interpreter { ("length".to_string(), Value::Builtin(BuiltinFn::ListLength)), ("get".to_string(), Value::Builtin(BuiltinFn::ListGet)), ("range".to_string(), Value::Builtin(BuiltinFn::ListRange)), + ( + "isEmpty".to_string(), + Value::Builtin(BuiltinFn::ListIsEmpty), + ), + ("find".to_string(), Value::Builtin(BuiltinFn::ListFind)), + ("any".to_string(), Value::Builtin(BuiltinFn::ListAny)), + ("all".to_string(), Value::Builtin(BuiltinFn::ListAll)), + ("take".to_string(), Value::Builtin(BuiltinFn::ListTake)), + ("drop".to_string(), Value::Builtin(BuiltinFn::ListDrop)), ])); env.define("List", list_module); @@ -730,6 +764,26 @@ impl Interpreter { ), ("chars".to_string(), Value::Builtin(BuiltinFn::StringChars)), ("lines".to_string(), Value::Builtin(BuiltinFn::StringLines)), + ( + "startsWith".to_string(), + Value::Builtin(BuiltinFn::StringStartsWith), + ), + ( + "endsWith".to_string(), + Value::Builtin(BuiltinFn::StringEndsWith), + ), + ( + "toUpper".to_string(), + Value::Builtin(BuiltinFn::StringToUpper), + ), + ( + "toLower".to_string(), + Value::Builtin(BuiltinFn::StringToLower), + ), + ( + "substring".to_string(), + Value::Builtin(BuiltinFn::StringSubstring), + ), ])); env.define("String", string_module); @@ -789,6 +843,19 @@ impl Interpreter { ), ])); env.define("Schema", schema_module); + + // Math module + let math_module = Value::Record(HashMap::from([ + ("abs".to_string(), Value::Builtin(BuiltinFn::MathAbs)), + ("min".to_string(), Value::Builtin(BuiltinFn::MathMin)), + ("max".to_string(), Value::Builtin(BuiltinFn::MathMax)), + ("sqrt".to_string(), Value::Builtin(BuiltinFn::MathSqrt)), + ("pow".to_string(), Value::Builtin(BuiltinFn::MathPow)), + ("floor".to_string(), Value::Builtin(BuiltinFn::MathFloor)), + ("ceil".to_string(), Value::Builtin(BuiltinFn::MathCeil)), + ("round".to_string(), Value::Builtin(BuiltinFn::MathRound)), + ])); + env.define("Math", math_module); } /// Execute a program @@ -1945,6 +2012,237 @@ impl Interpreter { _ => Err(err("Schema.getVersion: argument must be a Versioned value")), } } + + // Math operations + BuiltinFn::MathAbs => { + if args.len() != 1 { + return Err(err("Math.abs requires 1 argument")); + } + match &args[0] { + Value::Int(n) => Ok(EvalResult::Value(Value::Int(n.abs()))), + Value::Float(n) => Ok(EvalResult::Value(Value::Float(n.abs()))), + v => Err(err(&format!("Math.abs expects number, got {}", v.type_name()))), + } + } + + BuiltinFn::MathMin => { + let (a, b) = Self::expect_args_2::(&args, "Math.min", span) + .or_else(|_| { + // Try floats + if args.len() == 2 { + match (&args[0], &args[1]) { + (Value::Float(a), Value::Float(b)) => { + return Ok((0i64, 0i64)); // Placeholder - we'll handle below + } + _ => {} + } + } + Err(err("Math.min requires 2 number arguments")) + })?; + // Check if they were floats + match (&args[0], &args[1]) { + (Value::Float(a), Value::Float(b)) => { + Ok(EvalResult::Value(Value::Float(a.min(*b)))) + } + (Value::Int(a), Value::Int(b)) => { + Ok(EvalResult::Value(Value::Int((*a).min(*b)))) + } + _ => Err(err("Math.min requires 2 number arguments")), + } + } + + BuiltinFn::MathMax => { + match (&args[0], &args[1]) { + (Value::Float(a), Value::Float(b)) => { + Ok(EvalResult::Value(Value::Float(a.max(*b)))) + } + (Value::Int(a), Value::Int(b)) => { + Ok(EvalResult::Value(Value::Int((*a).max(*b)))) + } + _ => Err(err("Math.max requires 2 number arguments")), + } + } + + BuiltinFn::MathSqrt => { + if args.len() != 1 { + return Err(err("Math.sqrt requires 1 argument")); + } + match &args[0] { + Value::Int(n) => Ok(EvalResult::Value(Value::Float((*n as f64).sqrt()))), + Value::Float(n) => Ok(EvalResult::Value(Value::Float(n.sqrt()))), + v => Err(err(&format!("Math.sqrt expects number, got {}", v.type_name()))), + } + } + + BuiltinFn::MathPow => { + if args.len() != 2 { + return Err(err("Math.pow requires 2 arguments: base, exponent")); + } + match (&args[0], &args[1]) { + (Value::Int(base), Value::Int(exp)) => { + if *exp >= 0 { + Ok(EvalResult::Value(Value::Int(base.pow(*exp as u32)))) + } else { + Ok(EvalResult::Value(Value::Float((*base as f64).powi(*exp as i32)))) + } + } + (Value::Float(base), Value::Int(exp)) => { + Ok(EvalResult::Value(Value::Float(base.powi(*exp as i32)))) + } + (Value::Float(base), Value::Float(exp)) => { + Ok(EvalResult::Value(Value::Float(base.powf(*exp)))) + } + (Value::Int(base), Value::Float(exp)) => { + Ok(EvalResult::Value(Value::Float((*base as f64).powf(*exp)))) + } + _ => Err(err("Math.pow requires number arguments")), + } + } + + BuiltinFn::MathFloor => { + if args.len() != 1 { + return Err(err("Math.floor requires 1 argument")); + } + match &args[0] { + Value::Float(n) => Ok(EvalResult::Value(Value::Int(n.floor() as i64))), + Value::Int(n) => Ok(EvalResult::Value(Value::Int(*n))), + v => Err(err(&format!("Math.floor expects number, got {}", v.type_name()))), + } + } + + BuiltinFn::MathCeil => { + if args.len() != 1 { + return Err(err("Math.ceil requires 1 argument")); + } + match &args[0] { + Value::Float(n) => Ok(EvalResult::Value(Value::Int(n.ceil() as i64))), + Value::Int(n) => Ok(EvalResult::Value(Value::Int(*n))), + v => Err(err(&format!("Math.ceil expects number, got {}", v.type_name()))), + } + } + + BuiltinFn::MathRound => { + if args.len() != 1 { + return Err(err("Math.round requires 1 argument")); + } + match &args[0] { + Value::Float(n) => Ok(EvalResult::Value(Value::Int(n.round() as i64))), + Value::Int(n) => Ok(EvalResult::Value(Value::Int(*n))), + v => Err(err(&format!("Math.round expects number, got {}", v.type_name()))), + } + } + + // Additional List operations + BuiltinFn::ListIsEmpty => { + let list = Self::expect_arg_1::>(&args, "List.isEmpty", span)?; + Ok(EvalResult::Value(Value::Bool(list.is_empty()))) + } + + BuiltinFn::ListFind => { + let (list, func) = Self::expect_args_2::, Value>(&args, "List.find", span)?; + for item in list { + let v = self.eval_call_to_value(func.clone(), vec![item.clone()], span)?; + match v { + Value::Bool(true) => { + return Ok(EvalResult::Value(Value::Constructor { + name: "Some".to_string(), + fields: vec![item], + })); + } + Value::Bool(false) => {} + _ => return Err(err("List.find predicate must return Bool")), + } + } + Ok(EvalResult::Value(Value::Constructor { + name: "None".to_string(), + fields: vec![], + })) + } + + BuiltinFn::ListAny => { + let (list, func) = Self::expect_args_2::, Value>(&args, "List.any", span)?; + for item in list { + let v = self.eval_call_to_value(func.clone(), vec![item], span)?; + match v { + Value::Bool(true) => return Ok(EvalResult::Value(Value::Bool(true))), + Value::Bool(false) => {} + _ => return Err(err("List.any predicate must return Bool")), + } + } + Ok(EvalResult::Value(Value::Bool(false))) + } + + BuiltinFn::ListAll => { + let (list, func) = Self::expect_args_2::, Value>(&args, "List.all", span)?; + for item in list { + let v = self.eval_call_to_value(func.clone(), vec![item], span)?; + match v { + Value::Bool(false) => return Ok(EvalResult::Value(Value::Bool(false))), + Value::Bool(true) => {} + _ => return Err(err("List.all predicate must return Bool")), + } + } + Ok(EvalResult::Value(Value::Bool(true))) + } + + BuiltinFn::ListTake => { + let (list, n) = Self::expect_args_2::, i64>(&args, "List.take", span)?; + let n = n.max(0) as usize; + let result: Vec = list.into_iter().take(n).collect(); + Ok(EvalResult::Value(Value::List(result))) + } + + BuiltinFn::ListDrop => { + let (list, n) = Self::expect_args_2::, i64>(&args, "List.drop", span)?; + let n = n.max(0) as usize; + let result: Vec = list.into_iter().skip(n).collect(); + Ok(EvalResult::Value(Value::List(result))) + } + + // Additional String operations + BuiltinFn::StringStartsWith => { + let (s, prefix) = Self::expect_args_2::(&args, "String.startsWith", span)?; + Ok(EvalResult::Value(Value::Bool(s.starts_with(&prefix)))) + } + + BuiltinFn::StringEndsWith => { + let (s, suffix) = Self::expect_args_2::(&args, "String.endsWith", span)?; + Ok(EvalResult::Value(Value::Bool(s.ends_with(&suffix)))) + } + + BuiltinFn::StringToUpper => { + let s = Self::expect_arg_1::(&args, "String.toUpper", span)?; + Ok(EvalResult::Value(Value::String(s.to_uppercase()))) + } + + BuiltinFn::StringToLower => { + let s = Self::expect_arg_1::(&args, "String.toLower", span)?; + Ok(EvalResult::Value(Value::String(s.to_lowercase()))) + } + + BuiltinFn::StringSubstring => { + // String.substring(s, start, end) - end is exclusive + if args.len() != 3 { + return Err(err("String.substring requires 3 arguments: string, start, end")); + } + let s = match &args[0] { + Value::String(s) => s.clone(), + v => return Err(err(&format!("String.substring expects String, got {}", v.type_name()))), + }; + let start = match &args[1] { + Value::Int(n) => (*n).max(0) as usize, + v => return Err(err(&format!("String.substring expects Int for start, got {}", v.type_name()))), + }; + let end = match &args[2] { + Value::Int(n) => (*n).max(0) as usize, + v => return Err(err(&format!("String.substring expects Int for end, got {}", v.type_name()))), + }; + let chars: Vec = s.chars().collect(); + let end = end.min(chars.len()); + let start = start.min(end); + let result: String = chars[start..end].iter().collect(); + Ok(EvalResult::Value(Value::String(result))) + } } } diff --git a/src/main.rs b/src/main.rs index 0b651f5..24636c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1377,6 +1377,110 @@ c")"#; assert!(result.is_err()); assert!(result.unwrap_err().contains("downgrade")); } + + // Math module tests + #[test] + fn test_math_abs() { + let (result, _) = run_with_effects("let x = Math.abs(-42)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "42"); + let (result, _) = run_with_effects("let x = Math.abs(42)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "42"); + } + + #[test] + fn test_math_min_max() { + let (result, _) = run_with_effects("let x = Math.min(3, 7)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "3"); + let (result, _) = run_with_effects("let x = Math.max(3, 7)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "7"); + } + + #[test] + fn test_math_sqrt() { + let (result, _) = run_with_effects("let x = Math.sqrt(16)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "4"); + } + + #[test] + fn test_math_pow() { + let (result, _) = run_with_effects("let x = Math.pow(2, 10)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "1024"); + } + + #[test] + fn test_math_floor_ceil_round() { + let (result, _) = run_with_effects("let x = Math.floor(3.7)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "3"); + let (result, _) = run_with_effects("let x = Math.ceil(3.2)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "4"); + let (result, _) = run_with_effects("let x = Math.round(3.5)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "4"); + } + + // List module additional functions + #[test] + fn test_list_is_empty() { + let (result, _) = run_with_effects("let x = List.isEmpty([])", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "true"); + let (result, _) = run_with_effects("let x = List.isEmpty([1, 2])", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "false"); + } + + #[test] + fn test_list_find() { + let source = "let x = List.find([1, 2, 3, 4, 5], fn(x: Int): Bool => x > 3)"; + let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "Some(4)"); + let source = "let x = List.find([1, 2, 3], fn(x: Int): Bool => x > 10)"; + let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "None"); + } + + #[test] + fn test_list_any_all() { + let source = "let x = List.any([1, 2, 3], fn(x: Int): Bool => x > 2)"; + let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "true"); + let source = "let x = List.all([1, 2, 3], fn(x: Int): Bool => x > 0)"; + let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "true"); + let source = "let x = List.all([1, 2, 3], fn(x: Int): Bool => x > 2)"; + let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "false"); + } + + #[test] + fn test_list_take_drop() { + let (result, _) = run_with_effects("let x = List.take([1, 2, 3, 4, 5], 3)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "[1, 2, 3]"); + let (result, _) = run_with_effects("let x = List.drop([1, 2, 3, 4, 5], 2)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "[3, 4, 5]"); + } + + // String module additional functions + #[test] + fn test_string_starts_ends_with() { + let (result, _) = run_with_effects("let x = String.startsWith(\"hello\", \"he\")", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "true"); + let (result, _) = run_with_effects("let x = String.endsWith(\"hello\", \"lo\")", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "true"); + } + + #[test] + fn test_string_case_conversion() { + let (result, _) = run_with_effects("let x = String.toUpper(\"hello\")", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "\"HELLO\""); + let (result, _) = run_with_effects("let x = String.toLower(\"HELLO\")", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "\"hello\""); + } + + #[test] + fn test_string_substring() { + let (result, _) = run_with_effects("let x = String.substring(\"hello world\", 0, 5)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "\"hello\""); + let (result, _) = run_with_effects("let x = String.substring(\"hello\", 2, 4)", Value::Unit, Value::Unit).unwrap(); + assert_eq!(result, "\"ll\""); + } } // Diagnostic rendering tests diff --git a/src/types.rs b/src/types.rs index e028ec7..c2929a3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -974,6 +974,54 @@ impl TypeEnv { "range".to_string(), Type::function(vec![Type::Int, Type::Int], Type::List(Box::new(Type::Int))), ), + ( + "isEmpty".to_string(), + Type::function(vec![Type::List(Box::new(Type::var()))], Type::Bool), + ), + ( + "find".to_string(), + Type::function( + vec![ + Type::List(Box::new(Type::var())), + Type::function(vec![Type::var()], Type::Bool), + ], + Type::Option(Box::new(Type::var())), + ), + ), + ( + "any".to_string(), + Type::function( + vec![ + Type::List(Box::new(Type::var())), + Type::function(vec![Type::var()], Type::Bool), + ], + Type::Bool, + ), + ), + ( + "all".to_string(), + Type::function( + vec![ + Type::List(Box::new(Type::var())), + Type::function(vec![Type::var()], Type::Bool), + ], + Type::Bool, + ), + ), + ( + "take".to_string(), + Type::function( + vec![Type::List(Box::new(Type::var())), Type::Int], + Type::List(Box::new(Type::var())), + ), + ), + ( + "drop".to_string(), + Type::function( + vec![Type::List(Box::new(Type::var())), Type::Int], + Type::List(Box::new(Type::var())), + ), + ), ]); env.bind("List", TypeScheme::mono(list_module_type)); @@ -1017,6 +1065,26 @@ impl TypeEnv { "lines".to_string(), Type::function(vec![Type::String], Type::List(Box::new(Type::String))), ), + ( + "startsWith".to_string(), + Type::function(vec![Type::String, Type::String], Type::Bool), + ), + ( + "endsWith".to_string(), + Type::function(vec![Type::String, Type::String], Type::Bool), + ), + ( + "toUpper".to_string(), + Type::function(vec![Type::String], Type::String), + ), + ( + "toLower".to_string(), + Type::function(vec![Type::String], Type::String), + ), + ( + "substring".to_string(), + Type::function(vec![Type::String, Type::Int, Type::Int], Type::String), + ), ]); env.bind("String", TypeScheme::mono(string_module_type)); @@ -1138,6 +1206,43 @@ impl TypeEnv { ]); env.bind("Schema", TypeScheme::mono(schema_module_type)); + // Math module + let math_module_type = Type::Record(vec![ + ( + "abs".to_string(), + Type::function(vec![Type::var()], Type::var()), // Works on Int or Float + ), + ( + "min".to_string(), + Type::function(vec![Type::var(), Type::var()], Type::var()), + ), + ( + "max".to_string(), + Type::function(vec![Type::var(), Type::var()], Type::var()), + ), + ( + "sqrt".to_string(), + Type::function(vec![Type::var()], Type::Float), + ), + ( + "pow".to_string(), + Type::function(vec![Type::var(), Type::var()], Type::var()), + ), + ( + "floor".to_string(), + Type::function(vec![Type::var()], Type::Int), + ), + ( + "ceil".to_string(), + Type::function(vec![Type::var()], Type::Int), + ), + ( + "round".to_string(), + Type::function(vec![Type::var()], Type::Int), + ), + ]); + env.bind("Math", TypeScheme::mono(math_module_type)); + env }