//! Built-in functions for the doot language. pub mod async_ops; pub mod collections; pub mod crypto; pub mod io; pub mod strings; use crate::ast::Expr; use crate::evaluator::{EvalError, Evaluator, Value}; /// Dispatches a built-in function call. #[tracing::instrument(level = "trace", skip_all, fields(name))] pub fn call_builtin( eval: &mut Evaluator, name: &str, args: &[Value], arg_exprs: &[Expr], ) -> Result { match name { // Collections "map" => collections::map(eval, args, arg_exprs), "filter" => collections::filter(eval, args, arg_exprs), "fold" => collections::fold(eval, args, arg_exprs), "flatten" => collections::flatten(args), "concat" => collections::concat(args), "zip" => collections::zip(args), "enumerate" => collections::enumerate(args), "first" => collections::first(args), "last" => collections::last(args), "len" => collections::len(args), "contains" => collections::contains(args), "unique" => collections::unique(args), "sort" => collections::sort(args), "sort_by" => collections::sort_by(eval, args, arg_exprs), "reverse" => collections::reverse(args), "seq" => collections::seq(eval, args, arg_exprs), "batch" => collections::batch(eval, args, arg_exprs), // Strings "join" => strings::join(args), "split" => strings::split(args), "upper" => strings::upper(args), "lower" => strings::lower(args), "trim" => strings::trim(args), "replace" => strings::replace(args), "starts_with" => strings::starts_with(args), "ends_with" => strings::ends_with(args), "format" => strings::format(args), // Options "unwrap" => options_unwrap(args), "unwrap_or" => options_unwrap_or(args), "is_some" => options_is_some(args), "is_none" => options_is_none(args), // I/O "read_file" => io::read_file(args), "read_file_lines" => io::read_file_lines(args), "write_file" => io::write_file(args), "copy_file" => io::copy_file(args), "delete_file" => io::delete_file(args), "file_exists" => io::file_exists(args), "dir_exists" => io::dir_exists(args), "create_dir_all" => io::create_dir_all(args), "list_dir" => io::list_dir(args), "glob" => io::glob_files(args), "walk_dir" => io::walk_dir(args), "temp_dir" => io::temp_dir(), "temp_file" => io::temp_file(args), "is_symlink" => io::is_symlink(args), "read_link" => io::read_link(args), // Paths "path_join" => io::path_join(args), "path_parent" => io::path_parent(args), "path_filename" => io::path_filename(args), "path_extension" => io::path_extension(args), "home" => io::home(), "config_dir" => io::config_dir(), "config_path" => io::config_path(args), "data_dir" => io::data_dir(), "cache_dir" => io::cache_dir(), // Process "exec" => io::exec(args), "exec_with_status" => io::exec_with_status(args), "shell" => io::shell(args), "which" => io::which(args), // Serialization "to_json" => io::to_json(args), "from_json" => io::from_json(args), "to_toml" => io::to_toml(args), "from_toml" => io::from_toml(args), "to_yaml" => io::to_yaml(args), "from_yaml" => io::from_yaml(args), // Crypto "hash_file" => crypto::hash_file(args), "hash_str" => crypto::hash_str(args), "encrypt_age" => crypto::encrypt_age(args), "decrypt_age" => crypto::decrypt_age(args), // Async "all" => async_ops::all(args), "race" => async_ops::race(args), // Network "fetch" => async_ops::fetch(args), "fetch_json" => async_ops::fetch_json(args), "fetch_bytes" => async_ops::fetch_bytes(args), "post" => async_ops::post(args), "post_json" => async_ops::post_json(args), "download" => async_ops::download(args), // Environment "env" => env_get(args), // Debug "print" => print_values(args), "println" => println_values(args), "dbg" => dbg_values(args), _ => Err(EvalError::UndefinedFunction(name.to_string())), } } /// Dispatches a method call on a value. #[tracing::instrument(level = "trace", skip_all, fields(method))] pub fn call_method( eval: &mut Evaluator, obj: &Value, method: &str, args: &[Value], arg_exprs: &[Expr], ) -> Result { match obj { Value::List(items) => match method { "len" => Ok(Value::Int(items.len() as i64)), "first" => Ok(items.first().cloned().unwrap_or(Value::None)), "last" => Ok(items.last().cloned().unwrap_or(Value::None)), "contains" => { if let Some(needle) = args.first() { Ok(Value::Bool(items.iter().any(|v| values_equal(v, needle)))) } else { Ok(Value::Bool(false)) } } "map" => { let all_args = std::iter::once(obj.clone()) .chain(args.iter().cloned()) .collect::>(); collections::map(eval, &all_args, arg_exprs) } "filter" => { let all_args = std::iter::once(obj.clone()) .chain(args.iter().cloned()) .collect::>(); collections::filter(eval, &all_args, arg_exprs) } "fold" => { let all_args = std::iter::once(obj.clone()) .chain(args.iter().cloned()) .collect::>(); collections::fold(eval, &all_args, arg_exprs) } "join" => { let sep = args .first() .map(|v| match v { Value::Str(s) => s.as_str(), _ => "", }) .unwrap_or(""); let result = items .iter() .map(|v| v.to_string_repr()) .collect::>() .join(sep); Ok(Value::Str(result)) } "sort" => { let all_args = std::iter::once(obj.clone()) .chain(args.iter().cloned()) .collect::>(); collections::sort(&all_args) } "reverse" => { let mut reversed = items.clone(); reversed.reverse(); Ok(Value::List(reversed)) } "unique" => { let all_args = std::iter::once(obj.clone()) .chain(args.iter().cloned()) .collect::>(); collections::unique(&all_args) } _ => Err(EvalError::UndefinedFunction(format!("list.{}", method))), }, Value::Str(s) => match method { "len" => Ok(Value::Int(s.len() as i64)), "upper" => Ok(Value::Str(s.to_uppercase())), "lower" => Ok(Value::Str(s.to_lowercase())), "trim" => Ok(Value::Str(s.trim().to_string())), "split" => { let sep = args .first() .map(|v| match v { Value::Str(s) => s.as_str(), _ => " ", }) .unwrap_or(" "); let parts: Vec = s.split(sep).map(|p| Value::Str(p.to_string())).collect(); Ok(Value::List(parts)) } "replace" => { if args.len() >= 2 && let (Value::Str(from), Value::Str(to)) = (&args[0], &args[1]) { return Ok(Value::Str(s.replace(from, to))); } Ok(Value::Str(s.clone())) } "starts_with" => { if let Some(Value::Str(prefix)) = args.first() { Ok(Value::Bool(s.starts_with(prefix))) } else { Ok(Value::Bool(false)) } } "ends_with" => { if let Some(Value::Str(suffix)) = args.first() { Ok(Value::Bool(s.ends_with(suffix))) } else { Ok(Value::Bool(false)) } } "contains" => { if let Some(Value::Str(needle)) = args.first() { Ok(Value::Bool(s.contains(needle))) } else { Ok(Value::Bool(false)) } } _ => Err(EvalError::UndefinedFunction(format!("str.{}", method))), }, Value::Path(p) => match method { "parent" => Ok(Value::Path( p.parent().map(|p| p.to_path_buf()).unwrap_or_default(), )), "filename" => Ok(Value::Str( p.file_name() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_default(), )), "extension" => Ok(Value::Str( p.extension() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_default(), )), "exists" => Ok(Value::Bool(p.exists())), "is_file" => Ok(Value::Bool(p.is_file())), "is_dir" => Ok(Value::Bool(p.is_dir())), "join" => { if let Some(Value::Str(other)) = args.first() { Ok(Value::Path(p.join(other))) } else if let Some(Value::Path(other)) = args.first() { Ok(Value::Path(p.join(other))) } else { Ok(Value::Path(p.clone())) } } _ => Err(EvalError::UndefinedFunction(format!("path.{}", method))), }, Value::Struct(name, fields) => { if let Some(decl) = eval.env().get_struct(name).cloned() { for m in &decl.methods { if m.name == method { let mut method_args = vec![obj.clone()]; method_args.extend(args.iter().cloned()); let env_clone = eval.env().clone(); return eval.call_function(m, &env_clone, &method_args); } } } if let Some(field) = fields.get(method) && let Value::Function(func, env) = field { return eval.call_function(func, env, args); } Err(EvalError::FieldNotFound { ty: name.clone(), field: method.to_string(), }) } _ => Err(EvalError::TypeError(format!( "cannot call method {} on {}", method, obj.type_name() ))), } } fn values_equal(a: &Value, b: &Value) -> bool { match (a, b) { (Value::Int(x), Value::Int(y)) => x == y, (Value::Float(x), Value::Float(y)) => (x - y).abs() < f64::EPSILON, (Value::Str(x), Value::Str(y)) => x == y, (Value::Bool(x), Value::Bool(y)) => x == y, (Value::None, Value::None) => true, (Value::Enum(t1, v1), Value::Enum(t2, v2)) => t1 == t2 && v1 == v2, _ => false, } } fn options_unwrap(args: &[Value]) -> Result { match args.first() { Some(Value::None) => Err(EvalError::TypeError("unwrap called on none".to_string())), Some(v) => Ok(v.clone()), None => Err(EvalError::TypeError( "unwrap requires an argument".to_string(), )), } } fn options_unwrap_or(args: &[Value]) -> Result { match args.first() { Some(Value::None) => Ok(args.get(1).cloned().unwrap_or(Value::None)), Some(v) => Ok(v.clone()), None => Ok(args.get(1).cloned().unwrap_or(Value::None)), } } fn options_is_some(args: &[Value]) -> Result { Ok(Value::Bool(!matches!( args.first(), Some(Value::None) | None ))) } fn options_is_none(args: &[Value]) -> Result { Ok(Value::Bool(matches!( args.first(), Some(Value::None) | None ))) } fn env_get(args: &[Value]) -> Result { if let Some(Value::Str(key)) = args.first() { Ok(std::env::var(key).map(Value::Str).unwrap_or(Value::None)) } else { Ok(Value::None) } } fn print_values(args: &[Value]) -> Result { let output: Vec = args.iter().map(|v| v.to_string_repr()).collect(); print!("{}", output.join(" ")); Ok(Value::None) } fn println_values(args: &[Value]) -> Result { let output: Vec = args.iter().map(|v| v.to_string_repr()).collect(); println!("{}", output.join(" ")); Ok(Value::None) } fn dbg_values(args: &[Value]) -> Result { for (i, arg) in args.iter().enumerate() { eprintln!("[dbg {}] {:?}", i, arg); } // Return the last argument (or None) for easy chaining Ok(args.last().cloned().unwrap_or(Value::None)) }