use crate::runtime::{InterpreterError, Value};
use std::collections::HashMap;
use std::sync::{Arc, LazyLock, Mutex};
pub static OUTPUT_BUFFER: LazyLock<Mutex<String>> = LazyLock::new(|| Mutex::new(String::new()));
pub fn enable_output_capture() {
if let Ok(mut buf) = OUTPUT_BUFFER.lock() {
buf.clear();
}
}
pub fn get_captured_output() -> String {
if let Ok(mut buf) = OUTPUT_BUFFER.lock() {
let output = buf.clone();
buf.clear();
output
} else {
String::new()
}
}
pub fn is_output_capture_enabled() -> bool {
true
}
#[derive(Debug)]
pub struct BuiltinRegistry {
functions: HashMap<String, BuiltinFunction>,
}
pub type BuiltinFunction = fn(&[Value]) -> Result<Value, InterpreterError>;
impl Default for BuiltinRegistry {
fn default() -> Self {
Self::new()
}
}
impl BuiltinRegistry {
pub fn new() -> Self {
let mut registry = Self {
functions: HashMap::new(),
};
registry.register_all();
registry
}
fn register_all(&mut self) {
self.register("println", builtin_println);
self.register("print", builtin_print);
self.register("dbg", builtin_dbg);
self.register("len", builtin_len);
self.register("type_of", builtin_type_of);
self.register("is_nil", builtin_is_nil);
self.register("assert_eq", builtin_assert_eq);
self.register("assert", builtin_assert);
self.register("sqrt", builtin_sqrt);
self.register("pow", builtin_pow);
self.register("abs", builtin_abs);
self.register("min", builtin_min);
self.register("max", builtin_max);
self.register("floor", builtin_floor);
self.register("ceil", builtin_ceil);
self.register("round", builtin_round);
self.register("to_string", builtin_to_string);
self.register("parse_int", builtin_parse_int);
self.register("parse_float", builtin_parse_float);
self.register("push", builtin_push);
self.register("pop", builtin_pop);
self.register("reverse", builtin_reverse);
self.register("sort", builtin_sort);
self.register("env_args", builtin_env_args);
self.register("env_var", builtin_env_var);
self.register("env_set_var", builtin_env_set_var);
self.register("env_remove_var", builtin_env_remove_var);
self.register("env_vars", builtin_env_vars);
self.register("env_current_dir", builtin_env_current_dir);
self.register("env_set_current_dir", builtin_env_set_current_dir);
self.register("env_temp_dir", builtin_env_temp_dir);
self.register("fs_read", builtin_fs_read);
self.register("fs_write", builtin_fs_write);
self.register("fs_exists", builtin_fs_exists);
self.register("fs_create_dir", builtin_fs_create_dir);
self.register("fs_remove_file", builtin_fs_remove_file);
self.register("fs_remove_dir", builtin_fs_remove_dir);
self.register("fs_copy", builtin_fs_copy);
self.register("fs_rename", builtin_fs_rename);
self.register("fs_metadata", builtin_fs_metadata);
self.register("fs_read_dir", builtin_fs_read_dir);
self.register("fs_canonicalize", builtin_fs_canonicalize);
self.register("fs_is_file", builtin_fs_is_file);
self.register("path_join", builtin_path_join);
self.register("path_join_many", builtin_path_join_many);
self.register("path_parent", builtin_path_parent);
self.register("path_file_name", builtin_path_file_name);
self.register("path_file_stem", builtin_path_file_stem);
self.register("path_extension", builtin_path_extension);
self.register("path_is_absolute", builtin_path_is_absolute);
self.register("path_is_relative", builtin_path_is_relative);
self.register("path_canonicalize", builtin_path_canonicalize);
self.register("path_with_extension", builtin_path_with_extension);
self.register("path_with_file_name", builtin_path_with_file_name);
self.register("path_components", builtin_path_components);
self.register("path_normalize", builtin_path_normalize);
self.register("json_parse", builtin_json_parse);
self.register("json_stringify", builtin_json_stringify);
self.register("json_pretty", builtin_json_pretty);
self.register("json_read", builtin_json_read);
self.register("json_write", builtin_json_write);
self.register("json_validate", builtin_json_validate);
self.register("json_type", builtin_json_type);
self.register("json_merge", builtin_json_merge);
self.register("json_get", builtin_json_get);
self.register("json_set", builtin_json_set);
#[cfg(all(not(target_arch = "wasm32"), feature = "http-client"))]
{
self.register("http_get", builtin_http_get);
self.register("http_post", builtin_http_post);
self.register("http_put", builtin_http_put);
self.register("http_delete", builtin_http_delete);
}
}
fn register(&mut self, name: &str, func: BuiltinFunction) {
self.functions.insert(name.to_string(), func);
}
pub fn call(&self, name: &str, args: &[Value]) -> Result<Value, InterpreterError> {
if let Some(func) = self.functions.get(name) {
func(args)
} else {
Err(InterpreterError::RuntimeError(format!(
"Unknown builtin function: {name}"
)))
}
}
pub fn is_builtin(&self, name: &str) -> bool {
self.functions.contains_key(name)
}
}
#[inline]
fn require_no_args(args: &[Value], fn_name: &str) -> Result<(), InterpreterError> {
if !args.is_empty() {
return Err(InterpreterError::RuntimeError(format!(
"{fn_name}() expects no arguments"
)));
}
Ok(())
}
#[inline]
fn require_args_count(
args: &[Value],
expected: usize,
fn_name: &str,
) -> Result<(), InterpreterError> {
if args.len() != expected {
return Err(InterpreterError::RuntimeError(format!(
"{fn_name}() expects {expected} argument{}",
if expected == 1 { "" } else { "s" }
)));
}
Ok(())
}
#[inline]
fn extract_string_arg<'a>(args: &'a [Value], fn_name: &str) -> Result<&'a str, InterpreterError> {
require_args_count(args, 1, fn_name)?;
match &args[0] {
Value::String(s) => Ok(s.as_ref()),
_ => Err(InterpreterError::RuntimeError(format!(
"{fn_name}() expects a string argument"
))),
}
}
#[inline]
fn extract_two_string_args<'a>(
args: &'a [Value],
fn_name: &str,
) -> Result<(&'a str, &'a str), InterpreterError> {
require_args_count(args, 2, fn_name)?;
match (&args[0], &args[1]) {
(Value::String(s1), Value::String(s2)) => Ok((s1.as_ref(), s2.as_ref())),
_ => Err(InterpreterError::RuntimeError(format!(
"{fn_name}() expects two string arguments"
))),
}
}
fn builtin_println(args: &[Value]) -> Result<Value, InterpreterError> {
if args.is_empty() {
println!();
} else {
let output = args
.iter()
.map(|v| format!("{v}"))
.collect::<Vec<_>>()
.join(" ");
println!("{output}");
}
Ok(Value::nil())
}
fn builtin_print(args: &[Value]) -> Result<Value, InterpreterError> {
let output = args
.iter()
.map(|v| format!("{v}"))
.collect::<Vec<_>>()
.join(" ");
print!("{output}");
Ok(Value::nil())
}
fn builtin_dbg(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() == 1 {
println!("[DEBUG] {:?}", args[0]);
Ok(args[0].clone())
} else {
println!("[DEBUG] {args:?}");
Ok(Value::from_array(args.to_vec()))
}
}
fn builtin_len(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(format!(
"len() expects exactly 1 argument, got {}",
args.len()
)));
}
match &args[0] {
Value::String(s) => Ok(Value::Integer(s.len() as i64)),
Value::Array(arr) => Ok(Value::Integer(arr.len() as i64)),
Value::Object(fields) => Ok(Value::Integer(fields.len() as i64)),
Value::Range { start, end, .. } => match (start.as_ref(), end.as_ref()) {
(Value::Integer(s), Value::Integer(e)) => Ok(Value::Integer((e - s).abs())),
_ => Err(InterpreterError::RuntimeError(
"Range bounds must be integers for len()".to_string(),
)),
},
_ => Err(InterpreterError::RuntimeError(format!(
"len() not supported for {}",
args[0].type_name()
))),
}
}
fn builtin_type_of(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(format!(
"type_of() expects exactly 1 argument, got {}",
args.len()
)));
}
Ok(Value::from_string(args[0].type_name().to_string()))
}
fn builtin_is_nil(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(format!(
"is_nil() expects exactly 1 argument, got {}",
args.len()
)));
}
Ok(Value::Bool(matches!(args[0], Value::Nil)))
}
fn builtin_sqrt(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"sqrt() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::Integer(n) => Ok(Value::Float((*n as f64).sqrt())),
Value::Float(f) => Ok(Value::Float(f.sqrt())),
_ => Err(InterpreterError::RuntimeError(
"sqrt() expects a number".to_string(),
)),
}
}
fn builtin_pow(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"pow() expects exactly 2 arguments".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::Integer(base), Value::Integer(exp)) => {
if *exp >= 0 {
Ok(Value::Integer(base.pow(*exp as u32)))
} else {
Ok(Value::Float((*base as f64).powf(*exp as f64)))
}
}
(Value::Float(base), Value::Integer(exp)) => Ok(Value::Float(base.powf(*exp as f64))),
(Value::Integer(base), Value::Float(exp)) => Ok(Value::Float((*base as f64).powf(*exp))),
(Value::Float(base), Value::Float(exp)) => Ok(Value::Float(base.powf(*exp))),
_ => Err(InterpreterError::RuntimeError(
"pow() expects numeric arguments".to_string(),
)),
}
}
fn builtin_abs(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"abs() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::Integer(n) => Ok(Value::Integer(n.abs())),
Value::Float(f) => Ok(Value::Float(f.abs())),
_ => Err(InterpreterError::RuntimeError(
"abs() expects a number".to_string(),
)),
}
}
fn compare_less(a: &Value, b: &Value) -> Result<bool, InterpreterError> {
match (a, b) {
(Value::Integer(x), Value::Integer(y)) => Ok(y < x),
(Value::Float(x), Value::Float(y)) => Ok(y < x),
_ => Err(InterpreterError::RuntimeError(
"min() expects all arguments to be numbers of the same type".to_string(),
)),
}
}
fn builtin_min(args: &[Value]) -> Result<Value, InterpreterError> {
if args.is_empty() {
return Err(InterpreterError::RuntimeError(
"min() expects at least 1 argument".to_string(),
));
}
let mut min_val = &args[0];
for arg in &args[1..] {
if compare_less(min_val, arg)? {
min_val = arg;
}
}
Ok(min_val.clone())
}
fn compare_greater(a: &Value, b: &Value) -> Result<bool, InterpreterError> {
match (a, b) {
(Value::Integer(x), Value::Integer(y)) => Ok(y > x),
(Value::Float(x), Value::Float(y)) => Ok(y > x),
_ => Err(InterpreterError::RuntimeError(
"max() expects all arguments to be numbers of the same type".to_string(),
)),
}
}
fn builtin_max(args: &[Value]) -> Result<Value, InterpreterError> {
if args.is_empty() {
return Err(InterpreterError::RuntimeError(
"max() expects at least 1 argument".to_string(),
));
}
let mut max_val = &args[0];
for arg in &args[1..] {
if compare_greater(max_val, arg)? {
max_val = arg;
}
}
Ok(max_val.clone())
}
fn builtin_floor(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"floor() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::Integer(n) => Ok(Value::Integer(*n)),
Value::Float(f) => Ok(Value::Integer(f.floor() as i64)),
_ => Err(InterpreterError::RuntimeError(
"floor() expects a number".to_string(),
)),
}
}
fn builtin_ceil(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"ceil() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::Integer(n) => Ok(Value::Integer(*n)),
Value::Float(f) => Ok(Value::Integer(f.ceil() as i64)),
_ => Err(InterpreterError::RuntimeError(
"ceil() expects a number".to_string(),
)),
}
}
fn builtin_round(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"round() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::Integer(n) => Ok(Value::Integer(*n)),
Value::Float(f) => Ok(Value::Integer(f.round() as i64)),
_ => Err(InterpreterError::RuntimeError(
"round() expects a number".to_string(),
)),
}
}
fn builtin_to_string(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(format!(
"to_string() expects exactly 1 argument, got {}",
args.len()
)));
}
Ok(Value::from_string(format!("{}", args[0])))
}
fn builtin_parse_int(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"parse_int() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::String(s) => s
.parse::<i64>()
.map(Value::Integer)
.map_err(|_| InterpreterError::RuntimeError(format!("Cannot parse '{s}' as integer"))),
Value::Integer(n) => Ok(Value::Integer(*n)),
_ => Err(InterpreterError::RuntimeError(
"parse_int() expects a string".to_string(),
)),
}
}
fn builtin_parse_float(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"parse_float() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::String(s) => s
.parse::<f64>()
.map(Value::Float)
.map_err(|_| InterpreterError::RuntimeError(format!("Cannot parse '{s}' as float"))),
Value::Float(f) => Ok(Value::Float(*f)),
Value::Integer(n) => Ok(Value::Float(*n as f64)),
_ => Err(InterpreterError::RuntimeError(
"parse_float() expects a string".to_string(),
)),
}
}
fn builtin_push(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"push() expects exactly 2 arguments".to_string(),
));
}
match &args[0] {
Value::Array(arr) => {
let mut new_arr = arr.to_vec();
new_arr.push(args[1].clone());
Ok(Value::from_array(new_arr))
}
_ => Err(InterpreterError::RuntimeError(
"push() expects an array as first argument".to_string(),
)),
}
}
fn builtin_pop(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"pop() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::Array(arr) => {
let mut new_arr = arr.to_vec();
if let Some(val) = new_arr.pop() {
Ok(val)
} else {
Ok(Value::nil())
}
}
_ => Err(InterpreterError::RuntimeError(
"pop() expects an array".to_string(),
)),
}
}
fn builtin_reverse(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"reverse() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::Array(arr) => {
let mut new_arr = arr.to_vec();
new_arr.reverse();
Ok(Value::from_array(new_arr))
}
_ => Err(InterpreterError::RuntimeError(
"reverse() expects an array".to_string(),
)),
}
}
fn builtin_sort(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"sort() expects exactly 1 argument".to_string(),
));
}
match &args[0] {
Value::Array(arr) => {
let mut new_arr = arr.to_vec();
new_arr.sort_by(|a, b| match (a, b) {
(Value::Integer(x), Value::Integer(y)) => x.cmp(y),
(Value::Float(x), Value::Float(y)) => {
x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
}
(Value::String(x), Value::String(y)) => x.cmp(y),
_ => std::cmp::Ordering::Equal,
});
Ok(Value::from_array(new_arr))
}
_ => Err(InterpreterError::RuntimeError(
"sort() expects an array".to_string(),
)),
}
}
fn builtin_env_args(args: &[Value]) -> Result<Value, InterpreterError> {
if !args.is_empty() {
return Err(InterpreterError::RuntimeError(
"env_args() expects no arguments".to_string(),
));
}
let cmd_args: Vec<Value> = std::env::args().map(|s| Value::String(s.into())).collect();
Ok(Value::from_array(cmd_args))
}
fn builtin_env_var(args: &[Value]) -> Result<Value, InterpreterError> {
let key = extract_string_arg(args, "env_var")?;
match std::env::var(key) {
Ok(val) => Ok(Value::from_string(val)),
Err(_) => Err(InterpreterError::RuntimeError(format!(
"Environment variable '{key}' not found"
))),
}
}
fn builtin_env_set_var(args: &[Value]) -> Result<Value, InterpreterError> {
let (key, value) = extract_two_string_args(args, "env_set_var")?;
std::env::set_var(key, value);
Ok(Value::Nil)
}
fn builtin_env_remove_var(args: &[Value]) -> Result<Value, InterpreterError> {
let key = extract_string_arg(args, "env_remove_var")?;
std::env::remove_var(key);
Ok(Value::Nil)
}
fn builtin_env_vars(args: &[Value]) -> Result<Value, InterpreterError> {
require_no_args(args, "env_vars")?;
let vars: HashMap<String, Value> = std::env::vars()
.map(|(k, v)| (k, Value::from_string(v)))
.collect();
Ok(Value::Object(Arc::new(vars)))
}
fn builtin_env_current_dir(args: &[Value]) -> Result<Value, InterpreterError> {
require_no_args(args, "env_current_dir")?;
match std::env::current_dir() {
Ok(path) => Ok(Value::from_string(path.to_string_lossy().to_string())),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to get current directory: {e}"
))),
}
}
fn builtin_env_set_current_dir(args: &[Value]) -> Result<Value, InterpreterError> {
let path = extract_string_arg(args, "env_set_current_dir")?;
match std::env::set_current_dir(path) {
Ok(()) => Ok(Value::Nil),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to set current directory: {e}"
))),
}
}
fn builtin_env_temp_dir(args: &[Value]) -> Result<Value, InterpreterError> {
require_no_args(args, "env_temp_dir")?;
let temp = std::env::temp_dir();
Ok(Value::from_string(temp.to_string_lossy().to_string()))
}
fn builtin_fs_read(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"fs_read() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => match std::fs::read_to_string(path.as_ref()) {
Ok(content) => Ok(Value::from_string(content)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to read file: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"fs_read() expects a string argument".to_string(),
)),
}
}
fn builtin_fs_write(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"fs_write() expects 2 arguments".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(path), Value::String(content)) => {
match std::fs::write(path.as_ref(), content.as_ref()) {
Ok(()) => Ok(Value::Nil),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to write file: {e}"
))),
}
}
_ => Err(InterpreterError::RuntimeError(
"fs_write() expects two string arguments".to_string(),
)),
}
}
fn builtin_fs_exists(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"fs_exists() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => Ok(Value::Bool(std::path::Path::new(path.as_ref()).exists())),
_ => Err(InterpreterError::RuntimeError(
"fs_exists() expects a string argument".to_string(),
)),
}
}
fn builtin_fs_create_dir(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"fs_create_dir() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => match std::fs::create_dir(path.as_ref()) {
Ok(()) => Ok(Value::Nil),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to create directory: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"fs_create_dir() expects a string argument".to_string(),
)),
}
}
fn builtin_fs_remove_file(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"fs_remove_file() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => match std::fs::remove_file(path.as_ref()) {
Ok(()) => Ok(Value::Nil),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to remove file: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"fs_remove_file() expects a string argument".to_string(),
)),
}
}
fn builtin_fs_remove_dir(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"fs_remove_dir() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => match std::fs::remove_dir(path.as_ref()) {
Ok(()) => Ok(Value::Nil),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to remove directory: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"fs_remove_dir() expects a string argument".to_string(),
)),
}
}
fn builtin_fs_copy(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"fs_copy() expects 2 arguments".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(from), Value::String(to)) => match std::fs::copy(from.as_ref(), to.as_ref())
{
Ok(_) => Ok(Value::Nil),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to copy file: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"fs_copy() expects two string arguments".to_string(),
)),
}
}
fn builtin_fs_rename(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"fs_rename() expects 2 arguments".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(from), Value::String(to)) => {
match std::fs::rename(from.as_ref(), to.as_ref()) {
Ok(()) => Ok(Value::Nil),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to rename file: {e}"
))),
}
}
_ => Err(InterpreterError::RuntimeError(
"fs_rename() expects two string arguments".to_string(),
)),
}
}
fn builtin_fs_metadata(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"fs_metadata() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => match std::fs::metadata(path.as_ref()) {
Ok(meta) => {
let mut map = HashMap::new();
map.insert("size".to_string(), Value::Integer(meta.len() as i64));
map.insert("is_dir".to_string(), Value::Bool(meta.is_dir()));
map.insert("is_file".to_string(), Value::Bool(meta.is_file()));
Ok(Value::Object(Arc::new(map)))
}
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to get metadata: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"fs_metadata() expects a string argument".to_string(),
)),
}
}
fn builtin_fs_read_dir(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"fs_read_dir() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => match std::fs::read_dir(path.as_ref()) {
Ok(entries) => {
let names: Result<Vec<Value>, _> = entries
.map(|e| {
e.map(|entry| {
Value::from_string(entry.file_name().to_string_lossy().to_string())
})
})
.collect();
match names {
Ok(vec) => Ok(Value::from_array(vec)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to read directory: {e}"
))),
}
}
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to read directory: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"fs_read_dir() expects a string argument".to_string(),
)),
}
}
fn builtin_fs_canonicalize(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"fs_canonicalize() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => match std::fs::canonicalize(path.as_ref()) {
Ok(canonical) => Ok(Value::from_string(canonical.to_string_lossy().to_string())),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to canonicalize path: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"fs_canonicalize() expects a string argument".to_string(),
)),
}
}
fn builtin_fs_is_file(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"fs_is_file() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => Ok(Value::Bool(std::path::Path::new(path.as_ref()).is_file())),
_ => Err(InterpreterError::RuntimeError(
"fs_is_file() expects a string argument".to_string(),
)),
}
}
fn builtin_path_join(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"path_join() expects 2 arguments".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(base), Value::String(component)) => {
let path = std::path::Path::new(base.as_ref()).join(component.as_ref());
Ok(Value::from_string(path.to_string_lossy().to_string()))
}
_ => Err(InterpreterError::RuntimeError(
"path_join() expects two string arguments".to_string(),
)),
}
}
fn builtin_path_join_many(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_join_many() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::Array(components) => {
let path = build_path_from_components(components)?;
Ok(Value::from_string(path.to_string_lossy().to_string()))
}
_ => Err(InterpreterError::RuntimeError(
"path_join_many() expects an array argument".to_string(),
)),
}
}
fn build_path_from_components(
components: &[Value],
) -> Result<std::path::PathBuf, InterpreterError> {
let mut path = std::path::PathBuf::new();
for component in components {
match component {
Value::String(s) => path.push(s.as_ref()),
_ => {
return Err(InterpreterError::RuntimeError(
"path_join_many() expects array of strings".to_string(),
))
}
}
}
Ok(path)
}
fn builtin_path_parent(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_parent() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let p = std::path::Path::new(path.as_ref());
match p.parent() {
Some(parent) => Ok(Value::from_string(parent.to_string_lossy().to_string())),
None => Ok(Value::Nil),
}
}
_ => Err(InterpreterError::RuntimeError(
"path_parent() expects a string argument".to_string(),
)),
}
}
fn builtin_path_file_name(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_file_name() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let p = std::path::Path::new(path.as_ref());
match p.file_name() {
Some(name) => Ok(Value::from_string(name.to_string_lossy().to_string())),
None => Ok(Value::Nil),
}
}
_ => Err(InterpreterError::RuntimeError(
"path_file_name() expects a string argument".to_string(),
)),
}
}
fn builtin_path_file_stem(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_file_stem() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let p = std::path::Path::new(path.as_ref());
match p.file_stem() {
Some(stem) => Ok(Value::from_string(stem.to_string_lossy().to_string())),
None => Ok(Value::Nil),
}
}
_ => Err(InterpreterError::RuntimeError(
"path_file_stem() expects a string argument".to_string(),
)),
}
}
fn builtin_path_extension(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_extension() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let p = std::path::Path::new(path.as_ref());
match p.extension() {
Some(ext) => Ok(Value::from_string(ext.to_string_lossy().to_string())),
None => Ok(Value::Nil),
}
}
_ => Err(InterpreterError::RuntimeError(
"path_extension() expects a string argument".to_string(),
)),
}
}
fn builtin_path_is_absolute(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_is_absolute() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let p = std::path::Path::new(path.as_ref());
Ok(Value::Bool(p.is_absolute()))
}
_ => Err(InterpreterError::RuntimeError(
"path_is_absolute() expects a string argument".to_string(),
)),
}
}
fn builtin_path_is_relative(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_is_relative() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let p = std::path::Path::new(path.as_ref());
Ok(Value::Bool(p.is_relative()))
}
_ => Err(InterpreterError::RuntimeError(
"path_is_relative() expects a string argument".to_string(),
)),
}
}
fn builtin_path_canonicalize(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_canonicalize() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => match std::fs::canonicalize(path.as_ref()) {
Ok(canonical) => Ok(Value::from_string(canonical.to_string_lossy().to_string())),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"Failed to canonicalize path: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"path_canonicalize() expects a string argument".to_string(),
)),
}
}
fn builtin_path_with_extension(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"path_with_extension() expects 2 arguments".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(path), Value::String(ext)) => {
let p = std::path::Path::new(path.as_ref()).with_extension(ext.as_ref());
Ok(Value::from_string(p.to_string_lossy().to_string()))
}
_ => Err(InterpreterError::RuntimeError(
"path_with_extension() expects two string arguments".to_string(),
)),
}
}
fn builtin_path_with_file_name(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"path_with_file_name() expects 2 arguments".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(path), Value::String(name)) => {
let p = std::path::Path::new(path.as_ref()).with_file_name(name.as_ref());
Ok(Value::from_string(p.to_string_lossy().to_string()))
}
_ => Err(InterpreterError::RuntimeError(
"path_with_file_name() expects two string arguments".to_string(),
)),
}
}
fn builtin_path_components(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_components() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let p = std::path::Path::new(path.as_ref());
let components: Vec<Value> = p
.components()
.map(|c| Value::from_string(c.as_os_str().to_string_lossy().to_string()))
.collect();
Ok(Value::Array(components.into()))
}
_ => Err(InterpreterError::RuntimeError(
"path_components() expects a string argument".to_string(),
)),
}
}
fn builtin_path_normalize(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"path_normalize() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let p = std::path::Path::new(path.as_ref());
let mut normalized = std::path::PathBuf::new();
for component in p.components() {
match component {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
normalized.pop();
}
_ => normalized.push(component),
}
}
Ok(Value::from_string(normalized.to_string_lossy().to_string()))
}
_ => Err(InterpreterError::RuntimeError(
"path_normalize() expects a string argument".to_string(),
)),
}
}
fn json_value_to_ruchy(json: serde_json::Value) -> Value {
match json {
serde_json::Value::Null => Value::Nil,
serde_json::Value::Bool(b) => Value::Bool(b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Integer(i)
} else if let Some(f) = n.as_f64() {
Value::Float(f)
} else {
Value::Nil
}
}
serde_json::Value::String(s) => Value::from_string(s),
serde_json::Value::Array(arr) => {
let values: Vec<Value> = arr.into_iter().map(json_value_to_ruchy).collect();
Value::Array(values.into())
}
serde_json::Value::Object(obj) => {
let mut map = std::collections::HashMap::new();
for (k, v) in obj {
map.insert(k, json_value_to_ruchy(v));
}
Value::Object(std::sync::Arc::new(map))
}
}
}
fn ruchy_value_to_json(value: &Value) -> Result<serde_json::Value, InterpreterError> {
match value {
Value::Nil => Ok(serde_json::Value::Null),
Value::Bool(b) => Ok(serde_json::Value::Bool(*b)),
Value::Integer(i) => Ok(serde_json::json!(*i)),
Value::Float(f) => Ok(serde_json::json!(*f)),
Value::String(s) => Ok(serde_json::Value::String(s.to_string())),
Value::Array(arr) => convert_array_to_json(arr),
Value::Object(map) => convert_object_to_json(map),
Value::ObjectMut(map) => convert_object_mut_to_json(map),
_ => Err(InterpreterError::RuntimeError(format!(
"Cannot convert {value:?} to JSON"
))),
}
}
fn convert_array_to_json(arr: &[Value]) -> Result<serde_json::Value, InterpreterError> {
let json_arr: Result<Vec<serde_json::Value>, _> = arr.iter().map(ruchy_value_to_json).collect();
Ok(serde_json::Value::Array(json_arr?))
}
fn convert_object_to_json(
map: &std::collections::HashMap<String, Value>,
) -> Result<serde_json::Value, InterpreterError> {
let mut json_obj = serde_json::Map::new();
for (k, v) in map {
json_obj.insert(k.clone(), ruchy_value_to_json(v)?);
}
Ok(serde_json::Value::Object(json_obj))
}
fn convert_object_mut_to_json(
map: &std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, Value>>>,
) -> Result<serde_json::Value, InterpreterError> {
let guard = map
.lock()
.expect("Mutex poisoned: builtin registry lock is corrupted");
let mut json_obj = serde_json::Map::new();
for (k, v) in guard.iter() {
json_obj.insert(k.clone(), ruchy_value_to_json(v)?);
}
Ok(serde_json::Value::Object(json_obj))
}
fn builtin_json_parse(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"json_parse() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(s) => match serde_json::from_str::<serde_json::Value>(s) {
Ok(json) => Ok(json_value_to_ruchy(json)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"JSON parse error: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"json_parse() expects a string argument".to_string(),
)),
}
}
fn builtin_json_stringify(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"json_stringify() expects 1 argument".to_string(),
));
}
let json = ruchy_value_to_json(&args[0])?;
match serde_json::to_string(&json) {
Ok(s) => Ok(Value::from_string(s)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"JSON stringify error: {e}"
))),
}
}
fn builtin_json_pretty(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"json_pretty() expects 1 argument".to_string(),
));
}
let json = ruchy_value_to_json(&args[0])?;
match serde_json::to_string_pretty(&json) {
Ok(s) => Ok(Value::from_string(s)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"JSON pretty error: {e}"
))),
}
}
fn builtin_json_read(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"json_read() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| InterpreterError::RuntimeError(format!("Failed to read file: {e}")))?;
match serde_json::from_str::<serde_json::Value>(&content) {
Ok(json) => Ok(json_value_to_ruchy(json)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"JSON parse error: {e}"
))),
}
}
_ => Err(InterpreterError::RuntimeError(
"json_read() expects a string argument".to_string(),
)),
}
}
fn builtin_json_write(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"json_write() expects 2 arguments".to_string(),
));
}
match &args[0] {
Value::String(path) => {
let content = serialize_value_to_json_string(&args[1])?;
write_json_to_file(path, &content)?;
Ok(Value::Bool(true))
}
_ => Err(InterpreterError::RuntimeError(
"json_write() expects first argument to be string".to_string(),
)),
}
}
fn serialize_value_to_json_string(value: &Value) -> Result<String, InterpreterError> {
let json = ruchy_value_to_json(value)?;
serde_json::to_string_pretty(&json)
.map_err(|e| InterpreterError::RuntimeError(format!("JSON stringify error: {e}")))
}
fn write_json_to_file(path: &str, content: &str) -> Result<(), InterpreterError> {
std::fs::write(path, content)
.map_err(|e| InterpreterError::RuntimeError(format!("Failed to write file: {e}")))
}
fn builtin_json_validate(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"json_validate() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(s) => {
let is_valid = serde_json::from_str::<serde_json::Value>(s).is_ok();
Ok(Value::Bool(is_valid))
}
_ => Err(InterpreterError::RuntimeError(
"json_validate() expects a string argument".to_string(),
)),
}
}
fn builtin_json_type(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"json_type() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(s) => match serde_json::from_str::<serde_json::Value>(s) {
Ok(json) => {
let type_str = match json {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "boolean",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
};
Ok(Value::from_string(type_str.to_string()))
}
Err(e) => Err(InterpreterError::RuntimeError(format!(
"JSON parse error: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"json_type() expects a string argument".to_string(),
)),
}
}
fn builtin_json_merge(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"json_merge() expects 2 arguments".to_string(),
));
}
let json1 = ruchy_value_to_json(&args[0])?;
let json2 = ruchy_value_to_json(&args[1])?;
let merged = merge_json_values(json1, json2);
Ok(json_value_to_ruchy(merged))
}
fn merge_json_values(a: serde_json::Value, b: serde_json::Value) -> serde_json::Value {
match (a, b) {
(serde_json::Value::Object(mut a_map), serde_json::Value::Object(b_map)) => {
for (k, v) in b_map {
if let Some(a_val) = a_map.get_mut(&k) {
*a_val = merge_json_values(a_val.clone(), v);
} else {
a_map.insert(k, v);
}
}
serde_json::Value::Object(a_map)
}
(_, b_val) => b_val,
}
}
fn builtin_json_get(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"json_get() expects 2 arguments".to_string(),
));
}
let json = ruchy_value_to_json(&args[0])?;
match &args[1] {
Value::String(path) => {
let parts: Vec<&str> = path.split('.').collect();
let result = get_json_path(&json, &parts);
match result {
Some(val) => Ok(json_value_to_ruchy(val.clone())),
None => Ok(Value::Nil),
}
}
_ => Err(InterpreterError::RuntimeError(
"json_get() expects second argument to be string".to_string(),
)),
}
}
fn get_json_path<'a>(json: &'a serde_json::Value, path: &[&str]) -> Option<&'a serde_json::Value> {
if path.is_empty() {
return Some(json);
}
match json {
serde_json::Value::Object(map) => {
map.get(path[0]).and_then(|v| get_json_path(v, &path[1..]))
}
_ => None,
}
}
fn builtin_json_set(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 3 {
return Err(InterpreterError::RuntimeError(
"json_set() expects 3 arguments".to_string(),
));
}
let mut json = ruchy_value_to_json(&args[0])?;
let new_value = ruchy_value_to_json(&args[2])?;
match &args[1] {
Value::String(path) => {
let parts: Vec<&str> = path.split('.').collect();
set_json_path(&mut json, &parts, new_value);
Ok(json_value_to_ruchy(json))
}
_ => Err(InterpreterError::RuntimeError(
"json_set() expects second argument to be string".to_string(),
)),
}
}
fn set_json_path(json: &mut serde_json::Value, path: &[&str], value: serde_json::Value) {
if path.is_empty() {
*json = value;
return;
}
if path.len() == 1 {
set_json_value_at_key(json, path[0], value);
} else {
set_json_path_recursive(json, path, value);
}
}
fn set_json_value_at_key(json: &mut serde_json::Value, key: &str, value: serde_json::Value) {
if let serde_json::Value::Object(map) = json {
map.insert(key.to_string(), value);
}
}
fn set_json_path_recursive(json: &mut serde_json::Value, path: &[&str], value: serde_json::Value) {
if let serde_json::Value::Object(map) = json {
if let Some(next) = map.get_mut(path[0]) {
set_json_path(next, &path[1..], value);
}
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "http-client"))]
fn builtin_http_get(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"http_get() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(url) => match crate::stdlib::http::get(url) {
Ok(response) => Ok(Value::from_string(response)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"HTTP GET failed: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"http_get() expects a string URL".to_string(),
)),
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "http-client"))]
fn builtin_http_post(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"http_post() expects 2 arguments".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(url), Value::String(body)) => match crate::stdlib::http::post(url, body) {
Ok(response) => Ok(Value::from_string(response)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"HTTP POST failed: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"http_post() expects two string arguments".to_string(),
)),
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "http-client"))]
fn builtin_http_put(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 2 {
return Err(InterpreterError::RuntimeError(
"http_put() expects 2 arguments".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(url), Value::String(body)) => match crate::stdlib::http::put(url, body) {
Ok(response) => Ok(Value::from_string(response)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"HTTP PUT failed: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"http_put() expects two string arguments".to_string(),
)),
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "http-client"))]
fn builtin_http_delete(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() != 1 {
return Err(InterpreterError::RuntimeError(
"http_delete() expects 1 argument".to_string(),
));
}
match &args[0] {
Value::String(url) => match crate::stdlib::http::delete(url) {
Ok(response) => Ok(Value::from_string(response)),
Err(e) => Err(InterpreterError::RuntimeError(format!(
"HTTP DELETE failed: {e}"
))),
},
_ => Err(InterpreterError::RuntimeError(
"http_delete() expects a string URL".to_string(),
)),
}
}
fn builtin_assert_eq(args: &[Value]) -> Result<Value, InterpreterError> {
if args.len() < 2 {
return Err(InterpreterError::RuntimeError(
"assert_eq() expects at least 2 arguments (expected, actual)".to_string(),
));
}
let expected = &args[0];
let actual = &args[1];
let message = if args.len() > 2 {
format!("{}", args[2])
} else {
format!("Assertion failed: expected {expected:?}, got {actual:?}")
};
if expected == actual {
Ok(Value::nil())
} else {
Err(InterpreterError::AssertionFailed(message))
}
}
fn builtin_assert(args: &[Value]) -> Result<Value, InterpreterError> {
if args.is_empty() {
return Err(InterpreterError::RuntimeError(
"assert() expects at least 1 argument (condition)".to_string(),
));
}
let condition = &args[0];
let message = if args.len() > 1 {
format!("{}", args[1])
} else {
"Assertion failed".to_string()
};
match condition {
Value::Bool(true) => Ok(Value::nil()),
Value::Bool(false) => Err(InterpreterError::AssertionFailed(message)),
_ => Err(InterpreterError::RuntimeError(
"assert() expects a boolean condition".to_string(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builtin_println() {
let result = builtin_println(&[Value::from_string("test".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::nil());
}
#[test]
fn test_builtin_println_empty() {
let result = builtin_println(&[]).expect("builtin function should succeed in test");
assert_eq!(result, Value::nil());
}
#[test]
fn test_builtin_println_multiple_args() {
let args = vec![
Value::from_string("Hello".to_string()),
Value::from_string("World".to_string()),
Value::Integer(42),
];
let result = builtin_println(&args).expect("builtin function should succeed in test");
assert_eq!(result, Value::nil());
}
#[test]
fn test_builtin_println_different_types() {
let mut map = HashMap::new();
map.insert("key".to_string(), Value::Integer(1));
let args = vec![
Value::Integer(42),
Value::Float(std::f64::consts::PI),
Value::Bool(true),
Value::Object(Arc::new(map)),
];
let result = builtin_println(&args).expect("builtin function should succeed in test");
assert_eq!(result, Value::nil());
}
#[test]
fn test_builtin_len() {
let result = builtin_len(&[Value::from_string("hello".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(5));
let arr = Value::Array(Arc::from(vec![Value::Integer(1), Value::Integer(2)]));
let result = builtin_len(&[arr]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(2));
}
#[test]
fn test_builtin_len_object() {
let mut map = HashMap::new();
map.insert("a".to_string(), Value::Integer(1));
map.insert("b".to_string(), Value::Integer(2));
map.insert("c".to_string(), Value::Integer(3));
let result = builtin_len(&[Value::Object(Arc::new(map))])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(3));
}
#[test]
fn test_builtin_len_range() {
let range = Value::Range {
start: Box::new(Value::Integer(1)),
end: Box::new(Value::Integer(10)),
inclusive: false,
};
let result = builtin_len(&[range]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(9)); }
#[test]
fn test_builtin_len_wrong_type() {
let result = builtin_len(&[Value::Integer(42)]);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("len() not supported for"));
}
#[test]
fn test_builtin_type_of() {
let result = builtin_type_of(&[Value::Integer(42)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("integer".to_string()));
}
#[test]
fn test_builtin_type_of_nil() {
let result =
builtin_type_of(&[Value::Nil]).expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("nil".to_string()));
}
#[test]
fn test_builtin_type_of_bool() {
let result =
builtin_type_of(&[Value::Bool(true)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("boolean".to_string()));
}
#[test]
fn test_builtin_type_of_float() {
let result = builtin_type_of(&[Value::Float(std::f64::consts::PI)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("float".to_string()));
}
#[test]
fn test_builtin_type_of_string() {
let result = builtin_type_of(&[Value::from_string("hello".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("string".to_string()));
}
#[test]
fn test_builtin_type_of_array() {
let arr = Value::Array(Arc::from(vec![Value::Integer(1), Value::Integer(2)]));
let result = builtin_type_of(&[arr]).expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("array".to_string()));
}
#[test]
fn test_builtin_type_of_object() {
let mut map = HashMap::new();
map.insert("key".to_string(), Value::from_string("value".to_string()));
let result = builtin_type_of(&[Value::Object(Arc::new(map))])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("object".to_string()));
}
#[test]
fn test_builtin_sqrt() {
let result =
builtin_sqrt(&[Value::Integer(9)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Float(3.0));
}
#[test]
fn test_builtin_sqrt_negative() {
let result =
builtin_sqrt(&[Value::Integer(-1)]).expect("builtin function should succeed in test");
assert!(matches!(result, Value::Float(x) if x.is_nan()));
}
#[test]
fn test_builtin_sqrt_zero() {
let result =
builtin_sqrt(&[Value::Integer(0)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Float(0.0));
}
#[test]
fn test_builtin_sqrt_float() {
let result =
builtin_sqrt(&[Value::Float(16.0)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Float(4.0));
}
#[test]
fn test_builtin_sqrt_wrong_type() {
let result = builtin_sqrt(&[Value::from_string("not a number".to_string())]);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("sqrt() expects a number"));
}
#[test]
fn test_builtin_abs() {
let result =
builtin_abs(&[Value::Integer(-42)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(42));
}
#[test]
fn test_builtin_abs_negative_float() {
let result = builtin_abs(&[Value::Float(-std::f64::consts::PI)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Float(std::f64::consts::PI));
}
#[test]
fn test_builtin_abs_zero() {
let result =
builtin_abs(&[Value::Integer(0)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(0));
}
#[test]
fn test_builtin_abs_positive() {
let result =
builtin_abs(&[Value::Integer(42)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(42));
}
#[test]
fn test_builtin_abs_wrong_type() {
let result = builtin_abs(&[Value::from_string("not a number".to_string())]);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("abs() expects a number"));
}
#[test]
fn test_builtin_registry() {
let registry = BuiltinRegistry::new();
assert!(registry.is_builtin("println"));
assert!(registry.is_builtin("len"));
assert!(!registry.is_builtin("not_a_builtin"));
}
#[test]
fn test_builtin_reverse_array() {
let arr = vec![Value::Integer(1), Value::Integer(2), Value::Integer(3)];
let result = builtin_reverse(&[Value::Array(Arc::from(arr))])
.expect("builtin function should succeed in test");
if let Value::Array(reversed) = result {
assert_eq!(reversed.len(), 3);
assert_eq!(reversed[0], Value::Integer(3));
assert_eq!(reversed[1], Value::Integer(2));
assert_eq!(reversed[2], Value::Integer(1));
} else {
panic!("Expected Array");
}
}
#[test]
fn test_builtin_reverse_empty() {
let arr = vec![];
let result = builtin_reverse(&[Value::Array(Arc::from(arr))])
.expect("builtin function should succeed in test");
if let Value::Array(reversed) = result {
assert_eq!(reversed.len(), 0);
} else {
panic!("Expected Array");
}
}
#[test]
fn test_builtin_reverse_wrong_args() {
let result = builtin_reverse(&[]);
assert!(result.is_err());
let result = builtin_reverse(&[Value::Integer(42)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_sort_integers() {
let arr = vec![Value::Integer(3), Value::Integer(1), Value::Integer(2)];
let result = builtin_sort(&[Value::Array(Arc::from(arr))])
.expect("builtin function should succeed in test");
if let Value::Array(sorted) = result {
assert_eq!(sorted[0], Value::Integer(1));
assert_eq!(sorted[1], Value::Integer(2));
assert_eq!(sorted[2], Value::Integer(3));
} else {
panic!("Expected Array");
}
}
#[test]
fn test_builtin_sort_floats() {
let arr = vec![Value::Float(3.5), Value::Float(1.2), Value::Float(2.8)];
let result = builtin_sort(&[Value::Array(Arc::from(arr))])
.expect("builtin function should succeed in test");
if let Value::Array(sorted) = result {
assert_eq!(sorted[0], Value::Float(1.2));
assert_eq!(sorted[1], Value::Float(2.8));
assert_eq!(sorted[2], Value::Float(3.5));
} else {
panic!("Expected Array");
}
}
#[test]
fn test_builtin_sort_strings() {
let arr = vec![
Value::from_string("charlie".to_string()),
Value::from_string("alice".to_string()),
Value::from_string("bob".to_string()),
];
let result = builtin_sort(&[Value::Array(Arc::from(arr))])
.expect("builtin function should succeed in test");
if let Value::Array(sorted) = result {
assert_eq!(sorted[0], Value::from_string("alice".to_string()));
assert_eq!(sorted[1], Value::from_string("bob".to_string()));
assert_eq!(sorted[2], Value::from_string("charlie".to_string()));
} else {
panic!("Expected Array");
}
}
#[test]
fn test_builtin_sort_wrong_args() {
let result = builtin_sort(&[]);
assert!(result.is_err());
let result = builtin_sort(&[Value::Integer(42)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_env_args() {
let result = builtin_env_args(&[]).expect("builtin function should succeed in test");
if let Value::Array(args) = result {
assert!(!args.is_empty());
} else {
panic!("Expected Array");
}
}
#[test]
fn test_builtin_env_args_wrong_args() {
let result = builtin_env_args(&[Value::Integer(1)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_env_var_existing() {
std::env::set_var("RUCHY_TEST_VAR", "test_value");
let result = builtin_env_var(&[Value::from_string("RUCHY_TEST_VAR".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("test_value".to_string()));
std::env::remove_var("RUCHY_TEST_VAR");
}
#[test]
fn test_builtin_env_var_missing() {
std::env::remove_var("RUCHY_NONEXISTENT_VAR");
let result = builtin_env_var(&[Value::from_string("RUCHY_NONEXISTENT_VAR".to_string())]);
assert!(result.is_err(), "Should return error for missing variable");
if let Err(InterpreterError::RuntimeError(msg)) = result {
assert!(
msg.contains("not found"),
"Error message should mention 'not found'"
);
}
}
#[test]
fn test_builtin_env_var_wrong_args() {
let result = builtin_env_var(&[]);
assert!(result.is_err());
let result = builtin_env_var(&[Value::Integer(42)]);
assert!(result.is_err());
let result = builtin_env_var(&[Value::from_string("TEST".to_string()), Value::Integer(1)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_env_set_var() {
let result = builtin_env_set_var(&[
Value::from_string("RUCHY_TEST_SET_VAR".to_string()),
Value::from_string("new_value".to_string()),
])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
assert_eq!(
std::env::var("RUCHY_TEST_SET_VAR").expect("env var should be set in test"),
"new_value"
);
std::env::remove_var("RUCHY_TEST_SET_VAR");
}
#[test]
fn test_builtin_env_set_var_wrong_args() {
let result = builtin_env_set_var(&[]);
assert!(result.is_err());
let result = builtin_env_set_var(&[Value::from_string("TEST".to_string())]);
assert!(result.is_err());
let result =
builtin_env_set_var(&[Value::Integer(1), Value::from_string("value".to_string())]);
assert!(result.is_err());
}
#[test]
fn test_builtin_env_remove_var() {
std::env::set_var("RUCHY_TEST_REMOVE_VAR", "to_be_removed");
let result =
builtin_env_remove_var(&[Value::from_string("RUCHY_TEST_REMOVE_VAR".to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
assert!(std::env::var("RUCHY_TEST_REMOVE_VAR").is_err());
}
#[test]
fn test_builtin_env_remove_var_nonexistent() {
let result =
builtin_env_remove_var(&[Value::from_string("RUCHY_NEVER_EXISTED".to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
}
#[test]
fn test_builtin_env_remove_var_wrong_args() {
let result = builtin_env_remove_var(&[]);
assert!(result.is_err());
let result = builtin_env_remove_var(&[Value::Integer(42)]);
assert!(result.is_err());
let result =
builtin_env_remove_var(&[Value::from_string("TEST".to_string()), Value::Integer(1)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_env_vars() {
let result = builtin_env_vars(&[]).expect("builtin function should succeed in test");
if let Value::Object(vars) = result {
assert!(
!vars.is_empty(),
"Environment variables should not be empty"
);
assert!(vars.contains_key("PATH") || !vars.is_empty());
} else {
panic!("Expected Object");
}
}
#[test]
fn test_builtin_env_vars_wrong_args() {
let result = builtin_env_vars(&[Value::Integer(1)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_env_current_dir() {
let result = builtin_env_current_dir(&[]).expect("builtin function should succeed in test");
if let Value::String(dir) = result {
assert!(!dir.is_empty());
assert!(std::path::Path::new(dir.as_ref()).exists());
} else {
panic!("Expected String");
}
}
#[test]
fn test_builtin_env_current_dir_wrong_args() {
let result = builtin_env_current_dir(&[Value::Integer(1)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_env_set_current_dir() {
let original_dir =
std::env::current_dir().expect("builtin function should succeed in test");
let temp_dir = std::env::temp_dir();
let temp_dir_canonical = temp_dir.canonicalize().unwrap_or_else(|_| temp_dir.clone());
let result = builtin_env_set_current_dir(&[Value::from_string(
temp_dir.to_string_lossy().to_string(),
)])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
let current = std::env::current_dir()
.expect("should get current dir in test")
.canonicalize()
.expect("should canonicalize in test");
assert_eq!(current, temp_dir_canonical);
std::env::set_current_dir(&original_dir).expect("builtin function should succeed in test");
}
#[test]
fn test_builtin_env_set_current_dir_invalid() {
let result = builtin_env_set_current_dir(&[Value::from_string(
"/nonexistent/directory/xyz".to_string(),
)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_env_set_current_dir_wrong_args() {
let result = builtin_env_set_current_dir(&[]);
assert!(result.is_err());
let result = builtin_env_set_current_dir(&[Value::Integer(42)]);
assert!(result.is_err());
let result = builtin_env_set_current_dir(&[
Value::from_string("/tmp".to_string()),
Value::Integer(1),
]);
assert!(result.is_err());
}
#[test]
fn test_builtin_env_temp_dir() {
let result = builtin_env_temp_dir(&[]).expect("builtin function should succeed in test");
if let Value::String(temp_dir) = result {
assert!(!temp_dir.is_empty());
assert!(std::path::Path::new(temp_dir.as_ref()).exists());
} else {
panic!("Expected String");
}
}
#[test]
fn test_builtin_env_temp_dir_wrong_args() {
let result = builtin_env_temp_dir(&[Value::Integer(1)]);
assert!(result.is_err());
}
use tempfile::TempDir;
fn setup_test_file(content: &str) -> (TempDir, std::path::PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, content).expect("Failed to write test file");
(temp_dir, file_path)
}
#[test]
fn test_builtin_fs_read() {
let (temp_dir, file_path) = setup_test_file("Hello, World!");
let result =
builtin_fs_read(&[Value::from_string(file_path.to_string_lossy().to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::from_string("Hello, World!".to_string()));
drop(temp_dir);
}
#[test]
fn test_builtin_fs_read_missing_file() {
let result = builtin_fs_read(&[Value::from_string(
"/tmp/nonexistent_test_file_12345.txt".to_string(),
)]);
assert!(result.is_err(), "Should error for missing file");
}
#[test]
fn test_builtin_fs_write() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join("write_test.txt");
let result = builtin_fs_write(&[
Value::from_string(file_path.to_string_lossy().to_string()),
Value::from_string("Test content".to_string()),
])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
let content = std::fs::read_to_string(&file_path).expect("Failed to read file");
assert_eq!(content, "Test content");
drop(temp_dir);
}
#[test]
fn test_builtin_fs_exists_true() {
let (temp_dir, file_path) = setup_test_file("test");
let result =
builtin_fs_exists(&[Value::from_string(file_path.to_string_lossy().to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::from_bool(true));
drop(temp_dir);
}
#[test]
fn test_builtin_fs_exists_false() {
let result = builtin_fs_exists(&[Value::from_string(
"/tmp/nonexistent_file_xyz123.txt".to_string(),
)])
.expect("operation should succeed in test");
assert_eq!(result, Value::from_bool(false));
}
#[test]
fn test_builtin_fs_create_dir() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let dir_path = temp_dir.path().join("new_directory");
let result =
builtin_fs_create_dir(&[Value::from_string(dir_path.to_string_lossy().to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
assert!(dir_path.exists() && dir_path.is_dir());
drop(temp_dir);
}
#[test]
fn test_builtin_fs_remove_file() {
let (temp_dir, file_path) = setup_test_file("to be removed");
assert!(file_path.exists());
let result =
builtin_fs_remove_file(&[Value::from_string(file_path.to_string_lossy().to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
assert!(!file_path.exists(), "File should be removed");
drop(temp_dir);
}
#[test]
fn test_builtin_fs_remove_dir() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let dir_path = temp_dir.path().join("dir_to_remove");
std::fs::create_dir(&dir_path).expect("Failed to create directory");
assert!(dir_path.exists());
let result =
builtin_fs_remove_dir(&[Value::from_string(dir_path.to_string_lossy().to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
assert!(!dir_path.exists(), "Directory should be removed");
drop(temp_dir);
}
#[test]
fn test_builtin_fs_copy() {
let (temp_dir, source_path) = setup_test_file("content to copy");
let dest_path = temp_dir.path().join("copied_file.txt");
let result = builtin_fs_copy(&[
Value::from_string(source_path.to_string_lossy().to_string()),
Value::from_string(dest_path.to_string_lossy().to_string()),
])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
assert!(dest_path.exists());
let content = std::fs::read_to_string(&dest_path).expect("Failed to read copied file");
assert_eq!(content, "content to copy");
drop(temp_dir);
}
#[test]
fn test_builtin_fs_rename() {
let (temp_dir, old_path) = setup_test_file("content to rename");
let new_path = temp_dir.path().join("renamed_file.txt");
let result = builtin_fs_rename(&[
Value::from_string(old_path.to_string_lossy().to_string()),
Value::from_string(new_path.to_string_lossy().to_string()),
])
.expect("operation should succeed in test");
assert_eq!(result, Value::Nil);
assert!(!old_path.exists(), "Old file should not exist");
assert!(new_path.exists(), "New file should exist");
drop(temp_dir);
}
#[test]
fn test_builtin_fs_metadata() {
let (temp_dir, file_path) = setup_test_file("metadata test");
let result =
builtin_fs_metadata(&[Value::from_string(file_path.to_string_lossy().to_string())])
.expect("operation should succeed in test");
if let Value::Object(meta) = result {
assert!(meta.contains_key("is_file"));
assert!(meta.contains_key("is_dir"));
assert!(meta.contains_key("size")); assert_eq!(
meta.get("is_file")
.expect("is_file key should exist in metadata"),
&Value::Bool(true)
);
assert_eq!(
meta.get("is_dir")
.expect("is_dir key should exist in metadata"),
&Value::Bool(false)
);
} else {
panic!("Expected Object for metadata");
}
drop(temp_dir);
}
#[test]
fn test_builtin_fs_read_dir() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file1 = temp_dir.path().join("file1.txt");
let file2 = temp_dir.path().join("file2.txt");
std::fs::write(&file1, "test1").expect("Failed to write file1");
std::fs::write(&file2, "test2").expect("Failed to write file2");
let result = builtin_fs_read_dir(&[Value::from_string(
temp_dir.path().to_string_lossy().to_string(),
)])
.expect("operation should succeed in test");
if let Value::Array(entries) = result {
assert!(entries.len() >= 2, "Should have at least 2 entries");
} else {
panic!("Expected Array for read_dir");
}
drop(temp_dir);
}
#[test]
fn test_builtin_fs_canonicalize() {
let (temp_dir, file_path) = setup_test_file("canonicalize test");
let result =
builtin_fs_canonicalize(&[Value::from_string(file_path.to_string_lossy().to_string())])
.expect("operation should succeed in test");
if let Value::String(canonical_path) = result {
assert!(!canonical_path.is_empty());
} else {
panic!("Expected String for canonicalize");
}
drop(temp_dir);
}
#[test]
fn test_builtin_fs_is_file_true() {
let (temp_dir, file_path) = setup_test_file("is file test");
let result =
builtin_fs_is_file(&[Value::from_string(file_path.to_string_lossy().to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::from_bool(true));
drop(temp_dir);
}
#[test]
fn test_builtin_fs_is_file_false_for_dir() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let result = builtin_fs_is_file(&[Value::from_string(
temp_dir.path().to_string_lossy().to_string(),
)])
.expect("operation should succeed in test");
assert_eq!(result, Value::from_bool(false));
drop(temp_dir);
}
#[test]
fn test_builtin_fs_read_wrong_args() {
let result = builtin_fs_read(&[]);
assert!(result.is_err());
let result = builtin_fs_read(&[Value::Integer(42)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_fs_write_wrong_args() {
let result = builtin_fs_write(&[]);
assert!(result.is_err());
let result = builtin_fs_write(&[Value::from_string("test.txt".to_string())]);
assert!(result.is_err());
}
#[test]
fn test_builtin_pow_integers() {
let result = builtin_pow(&[Value::Integer(2), Value::Integer(3)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(8)); }
#[test]
fn test_builtin_pow_floats() {
let result = builtin_pow(&[Value::Float(2.0), Value::Float(3.0)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Float(8.0));
}
#[test]
fn test_builtin_pow_wrong_args() {
assert!(builtin_pow(&[]).is_err());
assert!(builtin_pow(&[Value::Integer(2)]).is_err());
assert!(
builtin_pow(&[Value::from_string("invalid".to_string()), Value::Integer(2)]).is_err()
);
}
#[test]
fn test_builtin_min_integers() {
let result = builtin_min(&[Value::Integer(5), Value::Integer(3)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(3));
}
#[test]
fn test_builtin_min_floats() {
let result = builtin_min(&[Value::Float(5.5), Value::Float(3.3)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Float(3.3));
}
#[test]
fn test_builtin_min_wrong_args() {
assert!(builtin_min(&[]).is_err());
assert!(builtin_min(&[Value::Integer(5)]).is_ok());
}
#[test]
fn test_builtin_max_integers() {
let result = builtin_max(&[Value::Integer(5), Value::Integer(3)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(5));
}
#[test]
fn test_builtin_max_floats() {
let result = builtin_max(&[Value::Float(5.5), Value::Float(3.3)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Float(5.5));
}
#[test]
fn test_builtin_max_wrong_args() {
assert!(builtin_max(&[]).is_err());
assert!(builtin_max(&[Value::Integer(5)]).is_ok());
}
#[test]
fn test_builtin_floor_positive() {
let result =
builtin_floor(&[Value::Float(3.7)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(3)); }
#[test]
fn test_builtin_floor_negative() {
let result =
builtin_floor(&[Value::Float(-3.7)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(-4)); }
#[test]
fn test_builtin_floor_wrong_args() {
assert!(builtin_floor(&[]).is_err());
assert!(builtin_floor(&[Value::from_string("invalid".to_string())]).is_err());
}
#[test]
fn test_builtin_ceil_positive() {
let result =
builtin_ceil(&[Value::Float(3.2)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(4)); }
#[test]
fn test_builtin_ceil_negative() {
let result =
builtin_ceil(&[Value::Float(-3.2)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(-3)); }
#[test]
fn test_builtin_ceil_wrong_args() {
assert!(builtin_ceil(&[]).is_err());
}
#[test]
fn test_builtin_round_positive() {
let result =
builtin_round(&[Value::Float(3.5)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(4)); }
#[test]
fn test_builtin_round_negative() {
let result =
builtin_round(&[Value::Float(-3.5)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(-4)); }
#[test]
fn test_builtin_round_wrong_args() {
assert!(builtin_round(&[]).is_err());
}
#[test]
fn test_builtin_to_string_integer() {
let result = builtin_to_string(&[Value::Integer(42)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("42".to_string()));
}
#[test]
fn test_builtin_to_string_float() {
let result = builtin_to_string(&[Value::Float(std::f64::consts::PI)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string(std::f64::consts::PI.to_string()));
}
#[test]
fn test_builtin_to_string_bool() {
let result = builtin_to_string(&[Value::from_bool(true)])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("true".to_string()));
}
#[test]
fn test_builtin_to_string_nil() {
let result =
builtin_to_string(&[Value::Nil]).expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("nil".to_string()));
}
#[test]
fn test_builtin_to_string_string_with_quotes() {
let result = builtin_to_string(&[Value::from_string("hello".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("\"hello\"".to_string()));
}
#[test]
fn test_builtin_to_string_array() {
let arr = vec![Value::Integer(1), Value::Integer(2), Value::Integer(3)];
let result = builtin_to_string(&[Value::from_array(arr)])
.expect("builtin function should succeed in test");
assert!(matches!(result, Value::String(_)));
}
#[test]
fn test_builtin_to_string_object() {
let mut map = HashMap::new();
map.insert("key".to_string(), Value::from_string("value".to_string()));
let result = builtin_to_string(&[Value::Object(Arc::new(map))])
.expect("builtin function should succeed in test");
assert!(matches!(result, Value::String(_)));
}
#[test]
fn test_builtin_parse_int_valid() {
let result = builtin_parse_int(&[Value::from_string("42".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(42));
}
#[test]
fn test_builtin_parse_int_invalid() {
assert!(builtin_parse_int(&[Value::from_string("not_a_number".to_string())]).is_err());
}
#[test]
fn test_builtin_parse_int_wrong_args() {
assert!(builtin_parse_int(&[]).is_err());
assert!(builtin_parse_int(&[Value::Integer(42)]).is_ok());
}
#[test]
fn test_builtin_parse_float_valid() {
let result = builtin_parse_float(&[Value::from_string(std::f64::consts::PI.to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Float(std::f64::consts::PI));
}
#[test]
fn test_builtin_parse_float_invalid() {
assert!(builtin_parse_float(&[Value::from_string("not_a_number".to_string())]).is_err());
}
#[test]
fn test_builtin_parse_float_wrong_args() {
assert!(builtin_parse_float(&[]).is_err());
}
#[test]
fn test_builtin_assert_true() {
let result = builtin_assert(&[Value::from_bool(true)]);
assert!(result.is_ok());
}
#[test]
fn test_builtin_assert_false() {
let result = builtin_assert(&[Value::from_bool(false)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_assert_wrong_args() {
assert!(builtin_assert(&[]).is_err());
}
#[test]
fn test_builtin_assert_eq_equal() {
let result = builtin_assert_eq(&[Value::Integer(42), Value::Integer(42)]);
assert!(result.is_ok());
}
#[test]
fn test_builtin_assert_eq_not_equal() {
let result = builtin_assert_eq(&[Value::Integer(42), Value::Integer(43)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_assert_eq_wrong_args() {
assert!(builtin_assert_eq(&[]).is_err());
assert!(builtin_assert_eq(&[Value::Integer(42)]).is_err());
}
#[test]
fn test_builtin_is_nil_true() {
let result =
builtin_is_nil(&[Value::Nil]).expect("builtin function should succeed in test");
assert_eq!(result, Value::from_bool(true));
}
#[test]
fn test_builtin_is_nil_false() {
let result =
builtin_is_nil(&[Value::Integer(42)]).expect("builtin function should succeed in test");
assert_eq!(result, Value::from_bool(false));
}
#[test]
fn test_builtin_is_nil_wrong_args() {
assert!(builtin_is_nil(&[]).is_err());
}
#[test]
fn test_builtin_print() {
let result = builtin_print(&[Value::from_string("Hello".to_string())]);
assert!(result.is_ok());
assert_eq!(result.expect("result should be Ok in test"), Value::Nil);
}
#[test]
fn test_builtin_print_empty() {
let result = builtin_print(&[]);
assert!(result.is_ok());
assert_eq!(result.expect("result should be Ok in test"), Value::Nil);
}
#[test]
fn test_builtin_print_multiple_args() {
let args = vec![
Value::from_string("Hello".to_string()),
Value::Integer(42),
Value::Bool(false),
];
let result = builtin_print(&args);
assert!(result.is_ok());
assert_eq!(result.expect("result should be Ok in test"), Value::Nil);
}
#[test]
fn test_builtin_dbg() {
let result = builtin_dbg(&[Value::Integer(42)]);
assert!(result.is_ok());
assert_eq!(
result.expect("result should be Ok in test"),
Value::Integer(42)
);
}
#[test]
fn test_builtin_dbg_multiple_args() {
let args = vec![Value::Integer(1), Value::Integer(2), Value::Integer(3)];
let result = builtin_dbg(&args);
assert!(result.is_ok());
match result.expect("result should be Ok in test") {
Value::Array(arr) => assert_eq!(arr.len(), 3),
_ => panic!("Expected array return from dbg with multiple args"),
}
}
#[test]
fn test_builtin_dbg_different_types() {
let mut map = HashMap::new();
map.insert("test".to_string(), Value::Integer(99));
let args = vec![
Value::Float(std::f64::consts::PI),
Value::from_string("debug".to_string()),
Value::Object(Arc::new(map)),
];
let result = builtin_dbg(&args);
assert!(result.is_ok());
assert!(matches!(
result.expect("result should be Ok in test"),
Value::Array(_)
));
}
#[test]
fn test_builtin_push_array() {
let arr = Value::Array(vec![Value::Integer(1), Value::Integer(2)].into());
let result = builtin_push(&[arr, Value::Integer(3)])
.expect("builtin function should succeed in test");
match result {
Value::Array(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items[2], Value::Integer(3));
}
_ => panic!("Expected array"),
}
}
#[test]
fn test_builtin_push_wrong_args() {
assert!(builtin_push(&[]).is_err());
assert!(builtin_push(&[Value::Integer(42)]).is_err());
}
#[test]
fn test_builtin_pop_array() {
let arr =
Value::Array(vec![Value::Integer(1), Value::Integer(2), Value::Integer(3)].into());
let result = builtin_pop(&[arr]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(3));
}
#[test]
fn test_builtin_pop_empty_array() {
let arr = Value::Array(vec![].into());
let result = builtin_pop(&[arr]).expect("builtin function should succeed in test");
assert_eq!(result, Value::Nil);
}
#[test]
fn test_builtin_pop_wrong_args() {
assert!(builtin_pop(&[]).is_err());
assert!(builtin_pop(&[Value::Integer(42)]).is_err());
}
#[test]
fn test_builtin_path_join() {
let result = builtin_path_join(&[
Value::from_string("/home".to_string()),
Value::from_string("user".to_string()),
])
.expect("operation should succeed in test");
assert!(matches!(result, Value::String(_)));
if let Value::String(s) = result {
assert!(s.as_ref().contains("home") && s.as_ref().contains("user"));
}
}
#[test]
fn test_builtin_path_join_wrong_args() {
assert!(builtin_path_join(&[]).is_err());
assert!(builtin_path_join(&[Value::from_string("a".to_string())]).is_err());
assert!(
builtin_path_join(&[Value::Integer(42), Value::from_string("b".to_string())]).is_err()
);
}
#[test]
fn test_builtin_path_join_many() {
let components = vec![
Value::from_string("home".to_string()),
Value::from_string("user".to_string()),
Value::from_string("docs".to_string()),
];
let result = builtin_path_join_many(&[Value::Array(components.into())])
.expect("builtin function should succeed in test");
assert!(matches!(result, Value::String(_)));
}
#[test]
fn test_builtin_path_join_many_wrong_args() {
assert!(builtin_path_join_many(&[]).is_err());
assert!(builtin_path_join_many(&[Value::Integer(42)]).is_err());
}
#[test]
fn test_builtin_path_parent() {
let result = builtin_path_parent(&[Value::from_string("/home/user/file.txt".to_string())])
.expect("builtin function should succeed in test");
assert!(matches!(result, Value::String(_)));
}
#[test]
fn test_builtin_path_parent_root() {
let result = builtin_path_parent(&[Value::from_string("/".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Nil);
}
#[test]
fn test_builtin_path_file_name() {
let result =
builtin_path_file_name(&[Value::from_string("/home/user/file.txt".to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::from_string("file.txt".to_string()));
}
#[test]
fn test_builtin_path_file_name_no_file() {
let result = builtin_path_file_name(&[Value::from_string("/".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Nil);
}
#[test]
fn test_builtin_path_file_stem() {
let result =
builtin_path_file_stem(&[Value::from_string("/home/user/file.txt".to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::from_string("file".to_string()));
}
#[test]
fn test_builtin_path_extension() {
let result =
builtin_path_extension(&[Value::from_string("/home/user/file.txt".to_string())])
.expect("operation should succeed in test");
assert_eq!(result, Value::from_string("txt".to_string()));
}
#[test]
fn test_builtin_path_extension_no_ext() {
let result = builtin_path_extension(&[Value::from_string("/home/user/file".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Nil);
}
#[test]
fn test_builtin_path_is_absolute() {
let result = builtin_path_is_absolute(&[Value::from_string("/home/user".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Bool(true));
let result2 = builtin_path_is_absolute(&[Value::from_string("relative/path".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result2, Value::Bool(false));
}
#[test]
fn test_builtin_path_is_relative() {
let result = builtin_path_is_relative(&[Value::from_string("relative/path".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Bool(true));
let result2 = builtin_path_is_relative(&[Value::from_string("/absolute".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result2, Value::Bool(false));
}
#[test]
fn test_builtin_path_canonicalize() {
use std::fs;
let temp_file = "/tmp/ruchy_test_canonicalize.txt";
fs::write(temp_file, "test").expect("builtin function should succeed in test");
let result = builtin_path_canonicalize(&[Value::from_string(temp_file.to_string())]);
fs::remove_file(temp_file).ok();
assert!(result.is_ok());
}
#[test]
fn test_builtin_path_canonicalize_nonexistent() {
let result = builtin_path_canonicalize(&[Value::from_string(
"/nonexistent/path/file.txt".to_string(),
)]);
assert!(result.is_err());
}
#[test]
fn test_builtin_path_with_extension() {
let result = builtin_path_with_extension(&[
Value::from_string("/home/user/file.txt".to_string()),
Value::from_string("rs".to_string()),
])
.expect("operation should succeed in test");
assert_eq!(result, Value::from_string("/home/user/file.rs".to_string()));
}
#[test]
fn test_builtin_path_with_file_name() {
let result = builtin_path_with_file_name(&[
Value::from_string("/home/user/file.txt".to_string()),
Value::from_string("newfile.rs".to_string()),
])
.expect("operation should succeed in test");
assert_eq!(
result,
Value::from_string("/home/user/newfile.rs".to_string())
);
}
#[test]
fn test_builtin_path_components() {
let result = builtin_path_components(&[Value::from_string("/home/user/docs".to_string())])
.expect("builtin function should succeed in test");
assert!(matches!(result, Value::Array(_)));
if let Value::Array(arr) = result {
assert!(arr.len() >= 3);
}
}
#[test]
fn test_builtin_path_normalize() {
let result =
builtin_path_normalize(&[Value::from_string("/home/./user/../docs".to_string())])
.expect("operation should succeed in test");
assert!(matches!(result, Value::String(_)));
if let Value::String(s) = result {
assert!(!s.as_ref().contains("./"));
}
}
#[test]
fn test_builtin_path_normalize_with_dots() {
let result = builtin_path_normalize(&[Value::from_string("/a/b/../c/./d".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("/a/c/d".to_string()));
}
#[test]
fn test_builtin_json_parse() {
let json_str = r#"{"name": "test", "value": 42}"#;
let result = builtin_json_parse(&[Value::from_string(json_str.to_string())])
.expect("builtin function should succeed in test");
assert!(matches!(result, Value::Object(_)));
}
#[test]
fn test_builtin_json_parse_invalid_json() {
let invalid_json = "{invalid json}";
assert!(builtin_json_parse(&[Value::from_string(invalid_json.to_string())]).is_err());
}
#[test]
fn test_builtin_json_parse_wrong_args() {
assert!(builtin_json_parse(&[]).is_err());
assert!(builtin_json_parse(&[Value::Integer(42)]).is_err());
}
#[test]
fn test_builtin_json_stringify() {
let obj = Value::Object(Arc::new(
[("key".to_string(), Value::from_string("value".to_string()))]
.iter()
.cloned()
.collect(),
));
let result =
builtin_json_stringify(&[obj]).expect("builtin function should succeed in test");
assert!(matches!(result, Value::String(_)));
}
#[test]
fn test_builtin_json_stringify_wrong_args() {
assert!(builtin_json_stringify(&[]).is_err());
}
#[test]
fn test_builtin_json_pretty() {
let obj = Value::Object(Arc::new(
[("key".to_string(), Value::Integer(42))]
.iter()
.cloned()
.collect(),
));
let result = builtin_json_pretty(&[obj]).expect("builtin function should succeed in test");
if let Value::String(s) = result {
assert!(s.contains('\n')); } else {
panic!("Expected String result");
}
}
#[test]
fn test_builtin_json_pretty_wrong_args() {
assert!(builtin_json_pretty(&[]).is_err());
}
#[test]
fn test_builtin_json_read() {
use std::io::Write;
let temp_path = "/tmp/test_json_read.json";
let mut file =
std::fs::File::create(temp_path).expect("builtin function should succeed in test");
write!(file, r#"{{"test": true}}"#).expect("builtin function should succeed in test");
drop(file);
let result = builtin_json_read(&[Value::from_string(temp_path.to_string())])
.expect("builtin function should succeed in test");
assert!(matches!(result, Value::Object(_)));
std::fs::remove_file(temp_path).expect("builtin function should succeed in test");
}
#[test]
fn test_builtin_json_read_file_not_found() {
assert!(
builtin_json_read(&[Value::from_string("/nonexistent/file.json".to_string())]).is_err()
);
}
#[test]
fn test_builtin_json_write() {
let temp_path = "/tmp/test_json_write.json";
let obj = Value::Object(Arc::new(
[("test".to_string(), Value::Bool(true))]
.iter()
.cloned()
.collect(),
));
let result = builtin_json_write(&[Value::from_string(temp_path.to_string()), obj])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Bool(true));
assert!(std::path::Path::new(temp_path).exists());
std::fs::remove_file(temp_path).expect("builtin function should succeed in test");
}
#[test]
fn test_builtin_json_write_wrong_args() {
assert!(builtin_json_write(&[]).is_err());
assert!(builtin_json_write(&[Value::Integer(42), Value::Bool(true)]).is_err());
}
#[test]
fn test_builtin_json_validate_valid() {
let valid_json = r#"{"key": "value"}"#;
let result = builtin_json_validate(&[Value::from_string(valid_json.to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Bool(true));
}
#[test]
fn test_builtin_json_validate_invalid() {
let invalid_json = "{invalid}";
let result = builtin_json_validate(&[Value::from_string(invalid_json.to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Bool(false));
}
#[test]
fn test_builtin_json_validate_wrong_args() {
assert!(builtin_json_validate(&[]).is_err());
assert!(builtin_json_validate(&[Value::Integer(42)]).is_err());
}
#[test]
fn test_builtin_json_type_null() {
let result = builtin_json_type(&[Value::from_string("null".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("null".to_string()));
}
#[test]
fn test_builtin_json_type_boolean() {
let result = builtin_json_type(&[Value::from_string("true".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("boolean".to_string()));
}
#[test]
fn test_builtin_json_type_number() {
let result = builtin_json_type(&[Value::from_string("42".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("number".to_string()));
}
#[test]
fn test_builtin_json_type_string() {
let result = builtin_json_type(&[Value::from_string(r#""test""#.to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("string".to_string()));
}
#[test]
fn test_builtin_json_type_array() {
let result = builtin_json_type(&[Value::from_string("[1,2,3]".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("array".to_string()));
}
#[test]
fn test_builtin_json_type_object() {
let result = builtin_json_type(&[Value::from_string(r#"{"key":"value"}"#.to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::from_string("object".to_string()));
}
#[test]
fn test_builtin_json_type_wrong_args() {
assert!(builtin_json_type(&[]).is_err());
assert!(builtin_json_type(&[Value::Integer(42)]).is_err());
}
#[test]
fn test_builtin_json_merge() {
let obj1 = Value::Object(Arc::new(
[("a".to_string(), Value::Integer(1))]
.iter()
.cloned()
.collect(),
));
let obj2 = Value::Object(Arc::new(
[("b".to_string(), Value::Integer(2))]
.iter()
.cloned()
.collect(),
));
let result =
builtin_json_merge(&[obj1, obj2]).expect("builtin function should succeed in test");
assert!(matches!(result, Value::Object(_)));
}
#[test]
fn test_builtin_json_merge_wrong_args() {
assert!(builtin_json_merge(&[]).is_err());
assert!(builtin_json_merge(&[Value::Integer(42)]).is_err());
}
#[test]
fn test_builtin_json_get() {
let obj = Value::Object(Arc::new(
[("key".to_string(), Value::Integer(42))]
.iter()
.cloned()
.collect(),
));
let result = builtin_json_get(&[obj, Value::from_string("key".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Integer(42));
}
#[test]
fn test_builtin_json_get_not_found() {
let obj = Value::Object(Arc::new(
[("key".to_string(), Value::Integer(42))]
.iter()
.cloned()
.collect(),
));
let result = builtin_json_get(&[obj, Value::from_string("nonexistent".to_string())])
.expect("builtin function should succeed in test");
assert_eq!(result, Value::Nil);
}
#[test]
fn test_builtin_json_get_wrong_args() {
assert!(builtin_json_get(&[]).is_err());
assert!(builtin_json_get(&[Value::Integer(42), Value::Integer(42)]).is_err());
}
#[test]
fn test_builtin_json_set() {
let obj = Value::Object(Arc::new(
[("key".to_string(), Value::Integer(42))]
.iter()
.cloned()
.collect(),
));
let result = builtin_json_set(&[
obj,
Value::from_string("key".to_string()),
Value::Integer(100),
])
.expect("operation should succeed in test");
assert!(matches!(result, Value::Object(_)));
}
#[test]
fn test_builtin_json_set_wrong_args() {
assert!(builtin_json_set(&[]).is_err());
assert!(
builtin_json_set(&[Value::Integer(42), Value::Integer(42), Value::Integer(42)])
.is_err()
);
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn test_abs_idempotent(n: i64) {
let val = Value::Integer(n);
let result1 = builtin_abs(&[val])
.expect("builtin function should succeed in test");
let result2 = builtin_abs(std::slice::from_ref(&result1))
.expect("builtin function should succeed in test");
prop_assert_eq!(result1, result2);
}
#[test]
fn test_min_max_consistency(a: i64, b: i64) {
let min_result = builtin_min(&[Value::Integer(a), Value::Integer(b)])
.expect("builtin function should succeed in test");
let max_result = builtin_max(&[Value::Integer(a), Value::Integer(b)])
.expect("builtin function should succeed in test");
prop_assert!(min_result == Value::Integer(a) || min_result == Value::Integer(b));
prop_assert!(max_result == Value::Integer(a) || max_result == Value::Integer(b));
match (min_result, max_result) {
(Value::Integer(min), Value::Integer(max)) => prop_assert!(min <= max),
_ => prop_assert!(false),
}
}
#[test]
fn test_to_string_parse_roundtrip(n: i64) {
let val = Value::Integer(n);
let str_val = builtin_to_string(&[val])
.expect("builtin function should succeed in test");
let parsed = builtin_parse_int(&[str_val])
.expect("builtin function should succeed in test");
prop_assert_eq!(parsed, Value::Integer(n));
}
}
}