feat: expand standard library with Math module and new functions

- Add Math module: abs, min, max, sqrt, pow, floor, ceil, round
- Add List functions: isEmpty, find, any, all, take, drop
- Add String functions: startsWith, endsWith, toUpper, toLower, substring
- Add 12 tests for new standard library functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 10:15:14 -05:00
parent 4eebaebb27
commit 961a861822
3 changed files with 507 additions and 0 deletions

View File

@@ -58,6 +58,31 @@ pub enum BuiltinFn {
Versioned, // Create versioned value: versioned("TypeName", 1, value) Versioned, // Create versioned value: versioned("TypeName", 1, value)
Migrate, // Migrate to version: migrate(versionedValue, targetVersion) Migrate, // Migrate to version: migrate(versionedValue, targetVersion)
GetVersion, // Get version number: getVersion(versionedValue) 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 /// Runtime value
@@ -708,6 +733,15 @@ impl Interpreter {
("length".to_string(), Value::Builtin(BuiltinFn::ListLength)), ("length".to_string(), Value::Builtin(BuiltinFn::ListLength)),
("get".to_string(), Value::Builtin(BuiltinFn::ListGet)), ("get".to_string(), Value::Builtin(BuiltinFn::ListGet)),
("range".to_string(), Value::Builtin(BuiltinFn::ListRange)), ("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); env.define("List", list_module);
@@ -730,6 +764,26 @@ impl Interpreter {
), ),
("chars".to_string(), Value::Builtin(BuiltinFn::StringChars)), ("chars".to_string(), Value::Builtin(BuiltinFn::StringChars)),
("lines".to_string(), Value::Builtin(BuiltinFn::StringLines)), ("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); env.define("String", string_module);
@@ -789,6 +843,19 @@ impl Interpreter {
), ),
])); ]));
env.define("Schema", schema_module); 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 /// Execute a program
@@ -1945,6 +2012,237 @@ impl Interpreter {
_ => Err(err("Schema.getVersion: argument must be a Versioned value")), _ => 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::<i64, i64>(&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::<Vec<Value>>(&args, "List.isEmpty", span)?;
Ok(EvalResult::Value(Value::Bool(list.is_empty())))
}
BuiltinFn::ListFind => {
let (list, func) = Self::expect_args_2::<Vec<Value>, 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::<Vec<Value>, 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::<Vec<Value>, 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::<Vec<Value>, i64>(&args, "List.take", span)?;
let n = n.max(0) as usize;
let result: Vec<Value> = list.into_iter().take(n).collect();
Ok(EvalResult::Value(Value::List(result)))
}
BuiltinFn::ListDrop => {
let (list, n) = Self::expect_args_2::<Vec<Value>, i64>(&args, "List.drop", span)?;
let n = n.max(0) as usize;
let result: Vec<Value> = list.into_iter().skip(n).collect();
Ok(EvalResult::Value(Value::List(result)))
}
// Additional String operations
BuiltinFn::StringStartsWith => {
let (s, prefix) = Self::expect_args_2::<String, String>(&args, "String.startsWith", span)?;
Ok(EvalResult::Value(Value::Bool(s.starts_with(&prefix))))
}
BuiltinFn::StringEndsWith => {
let (s, suffix) = Self::expect_args_2::<String, String>(&args, "String.endsWith", span)?;
Ok(EvalResult::Value(Value::Bool(s.ends_with(&suffix))))
}
BuiltinFn::StringToUpper => {
let s = Self::expect_arg_1::<String>(&args, "String.toUpper", span)?;
Ok(EvalResult::Value(Value::String(s.to_uppercase())))
}
BuiltinFn::StringToLower => {
let s = Self::expect_arg_1::<String>(&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<char> = 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)))
}
} }
} }

View File

@@ -1377,6 +1377,110 @@ c")"#;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("downgrade")); 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 // Diagnostic rendering tests

View File

@@ -974,6 +974,54 @@ impl TypeEnv {
"range".to_string(), "range".to_string(),
Type::function(vec![Type::Int, Type::Int], Type::List(Box::new(Type::Int))), 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)); env.bind("List", TypeScheme::mono(list_module_type));
@@ -1017,6 +1065,26 @@ impl TypeEnv {
"lines".to_string(), "lines".to_string(),
Type::function(vec![Type::String], Type::List(Box::new(Type::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)); env.bind("String", TypeScheme::mono(string_module_type));
@@ -1138,6 +1206,43 @@ impl TypeEnv {
]); ]);
env.bind("Schema", TypeScheme::mono(schema_module_type)); 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 env
} }