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:
@@ -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::<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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user