380 lines
13 KiB
Rust
380 lines
13 KiB
Rust
//! 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<Value, EvalError> {
|
|
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<Value, EvalError> {
|
|
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::<Vec<_>>();
|
|
collections::map(eval, &all_args, arg_exprs)
|
|
}
|
|
"filter" => {
|
|
let all_args = std::iter::once(obj.clone())
|
|
.chain(args.iter().cloned())
|
|
.collect::<Vec<_>>();
|
|
collections::filter(eval, &all_args, arg_exprs)
|
|
}
|
|
"fold" => {
|
|
let all_args = std::iter::once(obj.clone())
|
|
.chain(args.iter().cloned())
|
|
.collect::<Vec<_>>();
|
|
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::<Vec<_>>()
|
|
.join(sep);
|
|
Ok(Value::Str(result))
|
|
}
|
|
"sort" => {
|
|
let all_args = std::iter::once(obj.clone())
|
|
.chain(args.iter().cloned())
|
|
.collect::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
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<Value> = 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<Value, EvalError> {
|
|
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<Value, EvalError> {
|
|
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<Value, EvalError> {
|
|
Ok(Value::Bool(!matches!(
|
|
args.first(),
|
|
Some(Value::None) | None
|
|
)))
|
|
}
|
|
|
|
fn options_is_none(args: &[Value]) -> Result<Value, EvalError> {
|
|
Ok(Value::Bool(matches!(
|
|
args.first(),
|
|
Some(Value::None) | None
|
|
)))
|
|
}
|
|
|
|
fn env_get(args: &[Value]) -> Result<Value, EvalError> {
|
|
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<Value, EvalError> {
|
|
let output: Vec<String> = args.iter().map(|v| v.to_string_repr()).collect();
|
|
print!("{}", output.join(" "));
|
|
Ok(Value::None)
|
|
}
|
|
|
|
fn println_values(args: &[Value]) -> Result<Value, EvalError> {
|
|
let output: Vec<String> = args.iter().map(|v| v.to_string_repr()).collect();
|
|
println!("{}", output.join(" "));
|
|
Ok(Value::None)
|
|
}
|
|
|
|
fn dbg_values(args: &[Value]) -> Result<Value, EvalError> {
|
|
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))
|
|
}
|