diff --git a/Cargo.lock b/Cargo.lock index e47e1fa..308e380 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,7 +161,7 @@ dependencies = [ [[package]] name = "rulesxp" -version = "0.1.2" +version = "0.2.0" dependencies = [ "nom", "rustyline", diff --git a/Cargo.toml b/Cargo.toml index 2da9614..16081da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rulesxp" -version = "0.1.2" +version = "0.2.0" edition = "2024" rust-version = "1.90" description = "Multi-language rules expression evaluator supporting JSONLogic and Scheme with strict typing" diff --git a/README.md b/README.md index 0bd2daa..1e03b49 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,94 @@ let result = evaluator::eval(&expr, &mut env).unwrap(); println!("{}", result); // 6 ``` +### Registering Custom Builtins + +You can extend the evaluator with your own builtins using strongly-typed +Rust functions. These are registered as **builtin operations** on the +`Environment`. + +#### Fixed-arity builtins + +```rust +use rulesxp::{Error, ast::Value, evaluator}; + +// Infallible builtin: returns a bare i64 +fn add2(a: i64, b: i64) -> i64 { + a + b +} + +// Fallible builtin: returns Result +fn safe_div(a: i64, b: i64) -> Result { + if b == 0 { + Err(Error::EvalError("division by zero".into())) + } else { + Ok(a / b) + } +} + +let mut env = evaluator::create_global_env(); +env.register_builtin_operation::<(i64, i64)>("add2", add2); +env.register_builtin_operation::<(i64, i64)>("safe-div", safe_div); + +// Now you can call (add2 7 5) or (safe-div 6 3) from Scheme +// Or you can call {"add2" : [7, 5]} or {"safe-div" : [6, 3]} from JSONLogic +``` + +#### List and variadic builtins + +For list-style and variadic behavior, use the iterator-based +parameter types from `rulesxp::evaluator`. + +```rust +use rulesxp::{Error, ast::Value, evaluator}; +use rulesxp::builtinops::Arity; +use rulesxp::evaluator::{NumIter, ValueIter}; + +// Single list argument: (sum-list (list 1 2 3 4)) => 10 +fn sum_list(nums: NumIter<'_>) -> i64 { + nums.sum() +} + +// Variadic over all arguments: (count-numbers 1 "x" 2 #t 3) => 3 +fn count_numbers(args: ValueIter<'_>) -> i64 { + args.filter(|v| matches!(v, Value::Number(_))).count() as i64 +} + +let mut env = evaluator::create_global_env(); + +// List parameter from a single list argument +env.register_builtin_operation::<(NumIter<'static>,)>("sum-list", sum_list); + +// Variadic builtin with explicit arity metadata +env.register_variadic_builtin_operation::<(ValueIter<'static>,)>( + "count-numbers", + Arity::AtLeast(0), + count_numbers, +); +``` + +The typed registration APIs currently support: + +- **Parameter types** (as elements of the `Args` tuple): + - `i64` (number) + - `bool` (boolean) + - `&str` (borrowed string slices) + - `Value` (owned access to the raw AST value) + - `ValueIter<'_>` (iterate over `&Value` from a list/rest argument) + - `NumIter<'_>` (iterate over numeric elements as `i64`) + - `BoolIter<'_>` (iterate over boolean elements as `bool`) + - `StringIter<'_>` (iterate over string elements as `&str`) + +- **Return types**: + - `Result` + - `Result` where `T: Into` (for example `i64`, + `bool`, `&str`, arrays/vectors of those types, or `Vec`) + - bare `T` where `T: Into` (for infallible helpers, which are + automatically wrapped as `Ok(T)`) + +Arity is enforced automatically. Conversion errors yield `TypeError`, +and builtin errors are surfaced directly as `Error` values. + ## Current Status ### Implemented diff --git a/examples/repl.rs b/examples/repl.rs index c42a609..abeeeeb 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -40,7 +40,7 @@ fn run_repl() { let mut env = evaluator::create_global_env(); // Register custom function that can be called from user code for demonstration purposes - env.register_builtin_function("help", print_help); + env.register_builtin_operation::<()>("help", print_help); let mut jsonlogic_mode = false; @@ -58,7 +58,7 @@ fn run_repl() { // Handle special commands match line { ":help" => { - _ = print_help(&[]).is_ok(); + _ = print_help().is_ok(); continue; } ":env" => { @@ -144,11 +144,7 @@ fn run_repl() { } } -fn print_help(args: &[Value]) -> Result { - if !args.is_empty() { - return Err(Error::arity_error(0, args.len())); - } - +fn print_help() -> Result { println!("Mini Scheme Interpreter with JSONLogic Support:"); println!(" :help - Show this help message"); println!(" :env - Show current environment bindings"); diff --git a/src/ast.rs b/src/ast.rs index eb7726f..037843a 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,14 +1,18 @@ -/// This module defines the core Abstract Syntax Tree (AST) types and helper functions -/// for representing values in the interpreter. The main enum, [`Value`], covers -/// all Scheme data types, including numbers, symbols, strings, booleans, lists, built-in -/// and user-defined functions, and precompiled operations. Ergonomic helper functions -/// such as [`val`], [`sym`], and [`nil`] are provided for convenient AST construction -/// in both code and tests. The module also implements conversion traits for common Rust -/// types, making it easy to build Values from Rust literals, arrays, slices, and -/// vectors. Equality and display logic are customized to match Scheme semantics, including -/// round-trip compatibility for precompiled operations. +/* + This module defines the core Abstract Syntax Tree (AST) types and helper functions + for representing values in the interpreter. The main enum, [`Value`], covers + all Scheme data types, including numbers, symbols, strings, booleans, lists, built-in + and user-defined functions, and precompiled operations. Ergonomic helper functions + such as [`val`], [`sym`], and [`nil`] are provided for convenient AST construction + in both code and tests. The module also implements conversion traits for common Rust + types, making it easy to build Values from Rust literals, arrays, slices, and + vectors. Equality and display logic are customized to match Scheme semantics, including + round-trip compatibility for precompiled operations. +*/ + use crate::Error; use crate::builtinops::BuiltinOp; +use crate::evaluator::intooperation::OperationFn; /// Type alias for number values in interpreter pub(crate) type NumberType = i64; @@ -55,7 +59,7 @@ pub(crate) fn is_valid_symbol(name: &str) -> bool { /// - `val(42)` for values, `sym("name")` for symbols, `nil()` for empty lists /// - `val([1, 2, 3])` for homogeneous lists /// - `val(vec![sym("op"), val(42)])` for mixed lists -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum Value { /// Numbers (integers only) Number(NumberType), @@ -77,7 +81,10 @@ pub enum Value { /// Uses id string for equality comparison instead of function pointer BuiltinFunction { id: String, - func: fn(&[Value]) -> Result, + // Stored as an Arc to allow dynamic wrapping of typed Rust functions/closures. + // Trait object enables registering strongly typed functions (e.g. fn(i64, i64)->i64) + // that are automatically converted to the canonical evaluator signature. + func: std::sync::Arc, }, /// User-defined functions (params, body, closure env) Function { @@ -90,6 +97,42 @@ pub enum Value { Unspecified, } +impl std::fmt::Debug for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Number(n) => write!(f, "Number({n})"), + Value::Symbol(s) => write!(f, "Symbol({s})"), + Value::String(s) => write!(f, "String(\"{s}\")"), + Value::Bool(b) => write!(f, "Bool({b})"), + Value::List(list) => { + write!(f, "List(")?; + for (i, v) in list.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{v:?}")?; + } + write!(f, ")") + } + Value::PrecompiledOp { op_id, args, .. } => { + write!(f, "PrecompiledOp({op_id}, args=[")?; + for (i, a) in args.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{a:?}")?; + } + write!(f, "])") + } + Value::BuiltinFunction { id, .. } => write!(f, "BuiltinFunction({id})"), + Value::Function { params, body, .. } => { + write!(f, "Function(params={params:?}, body={body:?})") + } + Value::Unspecified => write!(f, "Unspecified"), + } + } +} + // From trait implementations for Value - enables .into() conversion impl From<&str> for Value { fn from(s: &str) -> Self { @@ -146,6 +189,32 @@ impl + Clone> From<&[T]> for Value { } } +// Fallible conversions from `Value` back into primitive Rust types. + +impl std::convert::TryInto for Value { + type Error = Error; + + fn try_into(self) -> Result { + if let Value::Number(n) = self { + Ok(n) + } else { + Err(Error::TypeError("expected number".into())) + } + } +} + +impl std::convert::TryInto for Value { + type Error = Error; + + fn try_into(self) -> Result { + if let Value::Bool(b) = self { + Ok(b) + } else { + Err(Error::TypeError("expected boolean".into())) + } + } +} + /// Helper function for creating symbols - works great in mixed lists! /// Accepts both &str and String via Into<&str> #[cfg_attr(not(test), expect(dead_code))] diff --git a/src/builtinops.rs b/src/builtinops.rs index 117fcab..75f6054 100644 --- a/src/builtinops.rs +++ b/src/builtinops.rs @@ -51,11 +51,13 @@ use crate::Error; use crate::ast::{NumberType, Value}; +use crate::evaluator::intooperation::{IntoOperation, IntoVariadicOperation, OperationFn}; use crate::evaluator::{ - Environment, eval_and, eval_define, eval_if, eval_lambda, eval_or, eval_quote, + Environment, NumIter, StringIter, ValueIter, eval_and, eval_define, eval_if, eval_lambda, + eval_or, eval_quote, }; use std::collections::HashMap; -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; /// Represents the expected number of arguments for an operation #[derive(Debug, Clone, PartialEq)] @@ -99,8 +101,9 @@ impl Arity { /// Represents the implementation of a built-in expression (function or special form) #[derive(Clone)] pub enum OpKind { - /// Regular function that takes arguments and returns a value - Function(fn(&[Value]) -> Result), + /// Regular function that takes evaluated arguments and returns a value + /// via the canonical erased builtin signature used by the evaluator. + Function(Arc), /// Special form that requires access to the environment, unevaluated arguments and current evaluation stack depth SpecialForm(fn(&[Value], &mut Environment, usize) -> Result), } @@ -117,9 +120,7 @@ impl std::fmt::Debug for OpKind { impl PartialEq for OpKind { fn eq(&self, other: &Self) -> bool { match (self, other) { - (OpKind::Function(f1), OpKind::Function(f2)) => { - std::ptr::eq(f1 as *const _, f2 as *const _) - } + (OpKind::Function(f1), OpKind::Function(f2)) => Arc::ptr_eq(f1, f2), (OpKind::SpecialForm(f1), OpKind::SpecialForm(f2)) => { std::ptr::eq(f1 as *const _, f2 as *const _) } @@ -168,25 +169,28 @@ impl BuiltinOp { // Macro to generate numeric comparison functions macro_rules! numeric_comparison { ($name:ident, $op:tt, $op_str:expr) => { - fn $name(args: &[Value]) -> Result { - // SCHEME-JSONLOGIC-STRICT: Require at least 2 arguments (both standards allow < 2 args but with different semantics) - if args.len() < 2 { - return Err(Error::arity_error(2, args.len())); + fn $name(first: NumberType, rest: NumIter<'_>) -> Result { + let mut iter = rest.peekable(); + + // SCHEME-JSONLOGIC-STRICT: Require at least 2 arguments (both + // standards allow < 2 args but with different semantics). + // If there is no second operand, we have only one argument. + if iter.peek().is_none() { + return Err(Error::arity_error(2, 1)); } - // Chain comparisons: all adjacent pairs must satisfy the comparison - for window in args.windows(2) { - match window { - [Value::Number(a), Value::Number(b)] => { - if !(a $op b) { - return Ok(Value::Bool(false)); - } - } - _ => return Err(Error::TypeError(concat!($op_str, " requires numbers").into())), + // Chain comparisons: all adjacent pairs must satisfy the comparison. + // Start with the first argument, then compare each subsequent + // element against the previous one. + let mut prev = first; + for current in iter { + if !(prev $op current) { + return Ok(false); } + prev = current; } - Ok(Value::Bool(true)) + Ok(true) } }; } @@ -198,391 +202,376 @@ numeric_comparison!(builtin_gt, >, ">"); numeric_comparison!(builtin_le, <=, "<="); numeric_comparison!(builtin_ge, >=, ">="); -fn builtin_add(args: &[Value]) -> Result { +fn builtin_add(args: NumIter<'_>) -> Result { let mut sum = 0 as NumberType; for arg in args { - if let Value::Number(n) = arg { - sum = sum - .checked_add(*n) - .ok_or_else(|| Error::EvalError("Integer overflow in addition".into()))?; - } else { - return Err(Error::TypeError("+ requires numbers".into())); - } + sum = sum + .checked_add(arg) + .ok_or_else(|| Error::EvalError("Integer overflow in addition".into()))?; } - Ok(Value::Number(sum)) + Ok(sum) } -fn builtin_sub(args: &[Value]) -> Result { - match args { - [] => Err(Error::arity_error(1, 0)), - [Value::Number(first)] => { - // Unary minus: check for overflow when negating - let result = first - .checked_neg() - .ok_or_else(|| Error::EvalError("Integer overflow in negation".into()))?; - Ok(Value::Number(result)) - } - [Value::Number(first), rest @ ..] => { - let mut result = *first; - for arg in rest { - if let Value::Number(n) = arg { - result = result.checked_sub(*n).ok_or_else(|| { - Error::EvalError("Integer overflow in subtraction".into()) - })?; - } else { - return Err(Error::TypeError("- requires numbers".into())); - } - } - Ok(Value::Number(result)) - } - _ => Err(Error::TypeError("- requires numbers".into())), - } -} +fn builtin_sub(first: NumberType, rest: NumIter<'_>) -> Result { + let mut iter = rest.peekable(); -fn builtin_mul(args: &[Value]) -> Result { - // SCHEME-STRICT: Require at least 1 argument (Scheme R7RS allows 0 args, returns 1) - if args.is_empty() { - return Err(Error::arity_error(1, 0)); + if iter.peek().is_none() { + return first + .checked_neg() + .ok_or_else(|| Error::EvalError("Integer overflow in negation".into())); } - let mut product = 1 as NumberType; - for arg in args { - if let Value::Number(n) = arg { - product = product - .checked_mul(*n) - .ok_or_else(|| Error::EvalError("Integer overflow in multiplication".into()))?; - } else { - return Err(Error::TypeError("* requires numbers".into())); - } + let mut result = first; + for n in iter { + result = result + .checked_sub(n) + .ok_or_else(|| Error::EvalError("Integer overflow in subtraction".into()))?; } - Ok(Value::Number(product)) + + Ok(result) } -fn builtin_car(args: &[Value]) -> Result { - match args { - [Value::List(list)] => match list.as_slice() { - [] => Err(Error::EvalError("car of empty list".into())), - [first, ..] => Ok(first.clone()), - }, - [_] => Err(Error::TypeError("car requires a list".into())), - _ => Err(Error::arity_error(1, args.len())), +// SCHEME-STRICT: Require at least 1 argument (Scheme R7RS allows 0 args, returns 1) +fn builtin_mul(first: NumberType, rest: NumIter<'_>) -> Result { + let mut product = first; + for n in rest { + product = product + .checked_mul(n) + .ok_or_else(|| Error::EvalError("Integer overflow in multiplication".into()))?; } + Ok(product) } -fn builtin_cdr(args: &[Value]) -> Result { - match args { - [Value::List(list)] => match list.as_slice() { - [] => Err(Error::EvalError("cdr of empty list".into())), - [_, rest @ ..] => Ok(Value::List(rest.to_vec())), - }, - [_] => Err(Error::TypeError("cdr requires a list".into())), - _ => Err(Error::arity_error(1, args.len())), +fn builtin_car(mut list: ValueIter<'_>) -> Result { + match list.next() { + Some(first) => Ok(first.clone()), + None => Err(Error::EvalError("car of empty list".into())), } } -fn builtin_cons(args: &[Value]) -> Result { - match args { - [first, Value::List(rest)] => { - let mut new_list = vec![first.clone()]; - new_list.extend_from_slice(rest); +fn builtin_cdr(mut list: ValueIter<'_>) -> Result { + let Some(_) = list.next() else { + return Err(Error::EvalError("cdr of empty list".into())); + }; + + let rest: Vec = list.cloned().collect(); + Ok(Value::List(rest)) +} + +fn builtin_cons(first: Value, rest: Value) -> Result { + match rest { + Value::List(tail) => { + let mut new_list = vec![first]; + new_list.extend_from_slice(&tail); Ok(Value::List(new_list)) } - [_, _] => Err(Error::TypeError( + _ => Err(Error::TypeError( // SCHEME-STRICT: Require second argument to be a list (Scheme R7RS allows improper lists) "cons requires a list as second argument".to_owned(), )), - _ => Err(Error::arity_error(2, args.len())), } } -fn builtin_list(args: &[Value]) -> Result { - Ok(Value::List(args.to_vec())) +fn builtin_list(args: ValueIter<'_>) -> Value { + Value::List(args.cloned().collect()) } -fn builtin_null(args: &[Value]) -> Result { - match args { - [value] => Ok(Value::Bool(value.is_nil())), - _ => Err(Error::arity_error(1, args.len())), - } +fn builtin_null(value: Value) -> bool { + value.is_nil() } -fn builtin_not(args: &[Value]) -> Result { - match args { - [Value::Bool(b)] => Ok(Value::Bool(!b)), - [_] => Err(Error::TypeError("not requires a boolean argument".into())), - _ => Err(Error::arity_error(1, args.len())), - } +fn builtin_not(b: bool) -> bool { + !b } -fn builtin_equal(args: &[Value]) -> Result { - match args { - [first, second] => { - // Scheme's equal? is structural equality for all types - // JSONLOGIC-STRICT: Reject type coercion - require same types for equality - match (first, second) { - (Value::Bool(_), Value::Bool(_)) - | (Value::Number(_), Value::Number(_)) - | (Value::String(_), Value::String(_)) - | (Value::Symbol(_), Value::Symbol(_)) - | (Value::List(_), Value::List(_)) => { - // Same types - use structural equality - Ok(Value::Bool(first == second)) - } - _ => { - // Different types or non-comparable types - reject type coercion - Err(Error::TypeError( - "JSONLOGIC-STRICT: Equality comparison requires arguments of the same comparable type (no type coercion)".to_owned(), - )) - } - } +fn builtin_equal(first: Value, second: Value) -> Result { + // Scheme's equal? is structural equality for all types + // JSONLOGIC-STRICT: Reject type coercion - require same types for equality + match (&first, &second) { + (Value::Bool(_), Value::Bool(_)) + | (Value::Number(_), Value::Number(_)) + | (Value::String(_), Value::String(_)) + | (Value::Symbol(_), Value::Symbol(_)) + | (Value::List(_), Value::List(_)) => { + // Same types - use structural equality + Ok(first == second) + } + _ => { + // Different types or non-comparable types - reject type coercion + Err(Error::TypeError( + "JSONLOGIC-STRICT: Equality comparison requires arguments of the same comparable type (no type coercion)".to_owned(), + )) } - _ => Err(Error::arity_error(2, args.len())), } } -fn builtin_string_append(args: &[Value]) -> Result { +fn builtin_string_append(args: StringIter<'_>) -> String { let mut result = String::new(); - for arg in args { - if let Value::String(s) = arg { - result.push_str(s); - } else { - return Err(Error::TypeError( - "string-append requires string arguments".into(), - )); - } + for s in args { + result.push_str(s); } - Ok(Value::String(result)) + result } -macro_rules! min_max_op { - ($name:ident, $op:ident, $initial:expr, $op_name:expr) => { - fn $name(args: &[Value]) -> Result { - if args.is_empty() { - return Err(Error::arity_error(1, 0)); - } - let mut result = $initial; - for arg in args { - if let Value::Number(n) = arg { - result = result.$op(*n); - } else { - return Err(Error::TypeError( - concat!($op_name, " requires number arguments").into(), - )); - } - } - Ok(Value::Number(result)) - } - }; +fn builtin_max(first: NumberType, rest: NumIter<'_>) -> NumberType { + let mut result = first; + for n in rest { + result = result.max(n); + } + result } -min_max_op!(builtin_max, max, NumberType::MIN, "max"); -min_max_op!(builtin_min, min, NumberType::MAX, "min"); - -fn builtin_error(args: &[Value]) -> Result { - // Convert a value to error message string - fn value_to_error_string(value: &Value) -> String { - match value { - Value::String(s) => s.clone(), // Remove quotes for error messages - _ => format!("{value}"), // Use Display trait for everything else - } +fn builtin_min(first: NumberType, rest: NumIter<'_>) -> NumberType { + let mut result = first; + for n in rest { + result = result.min(n); } + result +} - // Build multi-part error message - fn build_error_message(first: &Value, rest: &[Value]) -> String { - let mut message = value_to_error_string(first); - for arg in rest { - message.push(' '); - message.push_str(&value_to_error_string(arg)); - } - message +fn builtin_error(args: ValueIter<'_>) -> Result { + let parts: Vec = args + .map(|value| match value { + Value::String(s) => s.clone(), + _ => format!("{value}"), + }) + .collect(); + + let message = if parts.is_empty() { + "Error".to_string() + } else { + parts.join(" ") + }; + + Err(Error::EvalError(message)) +} + +/// Global registry of all built-in operations. +/// +/// We keep the registry layout as a single contiguous collection of +/// `BuiltinOp` values for ease of auditing, but the underlying +/// builtin implementations are wired through the same adapter layer +/// used for custom builtin registration. This is done once at +/// initialization time via a `LazyLock`. +static BUILTIN_OPS: LazyLock> = LazyLock::new(|| { + fn builtin_fixed(f: F) -> Arc + where + F: IntoOperation, + { + >::into_operation(f) } - match args { - [] => Err(Error::EvalError("Error".into())), - [single] => Err(Error::EvalError(value_to_error_string(single))), - [first, rest @ ..] => Err(Error::EvalError(build_error_message(first, rest))), + fn builtin_variadic(f: F) -> Arc + where + F: IntoVariadicOperation, + { + >::into_variadic_operation(f) } -} -/// Global registry of all built-in operations as a simple array -static BUILTIN_OPS: &[BuiltinOp] = &[ - // Arithmetic operations - BuiltinOp { - scheme_id: "+", - jsonlogic_id: "+", - op_kind: OpKind::Function(builtin_add), - arity: Arity::AtLeast(0), - }, - BuiltinOp { - scheme_id: "-", - jsonlogic_id: "-", - op_kind: OpKind::Function(builtin_sub), - arity: Arity::AtLeast(1), - }, - BuiltinOp { - scheme_id: "*", - jsonlogic_id: "*", - op_kind: OpKind::Function(builtin_mul), - arity: Arity::AtLeast(1), // SCHEME-STRICT: Scheme R7RS allows 0 arguments (returns 1) - }, - // Comparison operations - BuiltinOp { - scheme_id: ">", - jsonlogic_id: ">", - op_kind: OpKind::Function(builtin_gt), - arity: Arity::AtLeast(2), - }, - BuiltinOp { - scheme_id: ">=", - jsonlogic_id: ">=", - op_kind: OpKind::Function(builtin_ge), - arity: Arity::AtLeast(2), - }, - BuiltinOp { - scheme_id: "<", - jsonlogic_id: "<", - op_kind: OpKind::Function(builtin_lt), - arity: Arity::AtLeast(2), - }, - BuiltinOp { - scheme_id: "<=", - jsonlogic_id: "<=", - op_kind: OpKind::Function(builtin_le), - arity: Arity::AtLeast(2), - }, - BuiltinOp { - scheme_id: "=", - jsonlogic_id: "scheme-numeric-equals", - op_kind: OpKind::Function(builtin_eq), - arity: Arity::AtLeast(2), - }, - BuiltinOp { - scheme_id: "equal?", - jsonlogic_id: "===", - op_kind: OpKind::Function(builtin_equal), - arity: Arity::Exact(2), - }, - // Logical operations - BuiltinOp { - scheme_id: "not", - jsonlogic_id: "!", - op_kind: OpKind::Function(builtin_not), - arity: Arity::Exact(1), - }, - BuiltinOp { - scheme_id: "and", - jsonlogic_id: "and", - op_kind: OpKind::SpecialForm(eval_and), - arity: Arity::AtLeast(1), // SCHEME-STRICT: Scheme R7RS allows 0 arguments (returns #t) - }, - BuiltinOp { - scheme_id: "or", - jsonlogic_id: "or", - op_kind: OpKind::SpecialForm(eval_or), - arity: Arity::AtLeast(1), // SCHEME-STRICT: Scheme R7RS allows 0 arguments (returns #f) - }, - // Control flow - BuiltinOp { - scheme_id: "if", - jsonlogic_id: "if", - op_kind: OpKind::SpecialForm(eval_if), - // SCHEME-JSONLOGIC-STRICT: Require exactly 3 arguments - // (Scheme allows 2 args with undefined behavior, JSONLogic allows chaining with >3 args) - arity: Arity::Exact(3), - }, - // Special forms for language constructs - BuiltinOp { - scheme_id: "quote", - jsonlogic_id: "scheme-quote", - op_kind: OpKind::SpecialForm(eval_quote), - arity: Arity::Exact(1), - }, - BuiltinOp { - scheme_id: "define", - jsonlogic_id: "scheme-define", - op_kind: OpKind::SpecialForm(eval_define), - arity: Arity::Exact(2), - }, - BuiltinOp { - scheme_id: "lambda", - jsonlogic_id: "scheme-lambda", - op_kind: OpKind::SpecialForm(eval_lambda), - // SCHEME-STRICT: Only supports fixed-arity lambdas (lambda (a b c) body) - // Does not support variadic forms: (lambda args body) or (lambda (a . rest) body) - // Duplicate parameter names are prohibited per R7RS standard - arity: Arity::Exact(2), - }, - // List operations - BuiltinOp { - scheme_id: "car", - jsonlogic_id: "scheme-car", - op_kind: OpKind::Function(builtin_car), - arity: Arity::Exact(1), - }, - BuiltinOp { - scheme_id: "cdr", - jsonlogic_id: "scheme-cdr", - op_kind: OpKind::Function(builtin_cdr), - arity: Arity::Exact(1), - }, - BuiltinOp { - scheme_id: "cons", - jsonlogic_id: "scheme-cons", - op_kind: OpKind::Function(builtin_cons), - arity: Arity::Exact(2), - }, - BuiltinOp { - scheme_id: "list", - jsonlogic_id: "scheme-list", - op_kind: OpKind::Function(builtin_list), - arity: Arity::Any, - }, - BuiltinOp { - scheme_id: "null?", - jsonlogic_id: "scheme-null?", - op_kind: OpKind::Function(builtin_null), - arity: Arity::Exact(1), - }, - // String operations - BuiltinOp { - scheme_id: "string-append", - jsonlogic_id: "cat", - op_kind: OpKind::Function(builtin_string_append), - arity: Arity::Any, - }, - // Math operations - BuiltinOp { - scheme_id: "max", - jsonlogic_id: "max", - op_kind: OpKind::Function(builtin_max), - arity: Arity::AtLeast(1), - }, - BuiltinOp { - scheme_id: "min", - jsonlogic_id: "min", - op_kind: OpKind::Function(builtin_min), - arity: Arity::AtLeast(1), - }, - // Error handling - BuiltinOp { - scheme_id: "error", - jsonlogic_id: "scheme-error", - op_kind: OpKind::Function(builtin_error), - arity: Arity::Any, - }, -]; + vec![ + // Arithmetic operations + BuiltinOp { + scheme_id: "+", + jsonlogic_id: "+", + op_kind: OpKind::Function(builtin_variadic::<(NumIter<'static>,), _>(builtin_add)), + arity: Arity::AtLeast(0), + }, + BuiltinOp { + scheme_id: "-", + jsonlogic_id: "-", + op_kind: OpKind::Function(builtin_variadic::<(NumberType, NumIter<'static>), _>( + builtin_sub, + )), + arity: Arity::AtLeast(1), + }, + BuiltinOp { + scheme_id: "*", + jsonlogic_id: "*", + op_kind: OpKind::Function(builtin_variadic::<(NumberType, NumIter<'static>), _>( + builtin_mul, + )), + arity: Arity::AtLeast(1), // SCHEME-STRICT: Scheme R7RS allows 0 arguments (returns 1) + }, + // Comparison operations + BuiltinOp { + scheme_id: ">", + jsonlogic_id: ">", + op_kind: OpKind::Function(builtin_variadic::<(NumberType, NumIter<'static>), _>( + builtin_gt, + )), + arity: Arity::AtLeast(2), + }, + BuiltinOp { + scheme_id: ">=", + jsonlogic_id: ">=", + op_kind: OpKind::Function(builtin_variadic::<(NumberType, NumIter<'static>), _>( + builtin_ge, + )), + arity: Arity::AtLeast(2), + }, + BuiltinOp { + scheme_id: "<", + jsonlogic_id: "<", + op_kind: OpKind::Function(builtin_variadic::<(NumberType, NumIter<'static>), _>( + builtin_lt, + )), + arity: Arity::AtLeast(2), + }, + BuiltinOp { + scheme_id: "<=", + jsonlogic_id: "<=", + op_kind: OpKind::Function(builtin_variadic::<(NumberType, NumIter<'static>), _>( + builtin_le, + )), + arity: Arity::AtLeast(2), + }, + BuiltinOp { + scheme_id: "=", + jsonlogic_id: "scheme-numeric-equals", + op_kind: OpKind::Function(builtin_variadic::<(NumberType, NumIter<'static>), _>( + builtin_eq, + )), + arity: Arity::AtLeast(2), + }, + BuiltinOp { + scheme_id: "equal?", + jsonlogic_id: "===", + op_kind: OpKind::Function(builtin_fixed::<(Value, Value), _>(builtin_equal)), + arity: Arity::Exact(2), + }, + // Logical operations + BuiltinOp { + scheme_id: "not", + jsonlogic_id: "!", + op_kind: OpKind::Function(builtin_fixed::<(bool,), _>(builtin_not)), + arity: Arity::Exact(1), + }, + BuiltinOp { + scheme_id: "and", + jsonlogic_id: "and", + op_kind: OpKind::SpecialForm(eval_and), + arity: Arity::AtLeast(1), // SCHEME-STRICT: Scheme R7RS allows 0 arguments (returns #t) + }, + BuiltinOp { + scheme_id: "or", + jsonlogic_id: "or", + op_kind: OpKind::SpecialForm(eval_or), + arity: Arity::AtLeast(1), // SCHEME-STRICT: Scheme R7RS allows 0 arguments (returns #f) + }, + // Control flow + BuiltinOp { + scheme_id: "if", + jsonlogic_id: "if", + op_kind: OpKind::SpecialForm(eval_if), + // SCHEME-JSONLOGIC-STRICT: Require exactly 3 arguments + // (Scheme allows 2 args with undefined behavior, JSONLogic allows chaining with >3 args) + arity: Arity::Exact(3), + }, + // Special forms for language constructs + BuiltinOp { + scheme_id: "quote", + jsonlogic_id: "scheme-quote", + op_kind: OpKind::SpecialForm(eval_quote), + arity: Arity::Exact(1), + }, + BuiltinOp { + scheme_id: "define", + jsonlogic_id: "scheme-define", + op_kind: OpKind::SpecialForm(eval_define), + arity: Arity::Exact(2), + }, + BuiltinOp { + scheme_id: "lambda", + jsonlogic_id: "scheme-lambda", + op_kind: OpKind::SpecialForm(eval_lambda), + // SCHEME-STRICT: Only supports fixed-arity lambdas (lambda (a b c) body) + // Does not support variadic forms: (lambda args body) or (lambda (a . rest) body) + // Duplicate parameter names are prohibited per R7RS standard + arity: Arity::Exact(2), + }, + // List operations + BuiltinOp { + scheme_id: "car", + jsonlogic_id: "scheme-car", + op_kind: OpKind::Function(builtin_fixed::<(ValueIter<'static>,), _>(builtin_car)), + arity: Arity::Exact(1), + }, + BuiltinOp { + scheme_id: "cdr", + jsonlogic_id: "scheme-cdr", + op_kind: OpKind::Function(builtin_fixed::<(ValueIter<'static>,), _>(builtin_cdr)), + arity: Arity::Exact(1), + }, + BuiltinOp { + scheme_id: "cons", + jsonlogic_id: "scheme-cons", + op_kind: OpKind::Function(builtin_fixed::<(Value, Value), _>(builtin_cons)), + arity: Arity::Exact(2), + }, + BuiltinOp { + scheme_id: "list", + jsonlogic_id: "scheme-list", + op_kind: OpKind::Function(builtin_variadic::<(ValueIter<'static>,), _>(builtin_list)), + arity: Arity::Any, + }, + BuiltinOp { + scheme_id: "null?", + jsonlogic_id: "scheme-null?", + op_kind: OpKind::Function(builtin_fixed::<(Value,), _>(builtin_null)), + arity: Arity::Exact(1), + }, + // String operations + BuiltinOp { + scheme_id: "string-append", + jsonlogic_id: "cat", + op_kind: OpKind::Function(builtin_variadic::<(StringIter<'static>,), _>( + builtin_string_append, + )), + arity: Arity::Any, + }, + // Math operations + BuiltinOp { + scheme_id: "max", + jsonlogic_id: "max", + op_kind: OpKind::Function(builtin_variadic::<(NumberType, NumIter<'static>), _>( + builtin_max, + )), + arity: Arity::AtLeast(1), + }, + BuiltinOp { + scheme_id: "min", + jsonlogic_id: "min", + op_kind: OpKind::Function(builtin_variadic::<(NumberType, NumIter<'static>), _>( + builtin_min, + )), + arity: Arity::AtLeast(1), + }, + // Error handling + BuiltinOp { + scheme_id: "error", + jsonlogic_id: "scheme-error", + op_kind: OpKind::Function(builtin_variadic::<(ValueIter<'static>,), _>(builtin_error)), + arity: Arity::Any, + }, + ] +}); /// Lazy static map from scheme_id to BuiltinOp (private - use find_builtin_op_by_scheme_id) -static BUILTIN_SCHEME: LazyLock> = - LazyLock::new(|| BUILTIN_OPS.iter().map(|op| (op.scheme_id, op)).collect()); +static BUILTIN_SCHEME: LazyLock> = LazyLock::new(|| { + let ops: &'static [BuiltinOp] = BUILTIN_OPS.as_slice(); + ops.iter().map(|op| (op.scheme_id, op)).collect() +}); /// Lazy static map from jsonlogic_id to BuiltinOp (private - use find_builtin_op_by_jsonlogic_id) static BUILTIN_JSONLOGIC: LazyLock> = - LazyLock::new(|| BUILTIN_OPS.iter().map(|op| (op.jsonlogic_id, op)).collect()); + LazyLock::new(|| { + let ops: &'static [BuiltinOp] = BUILTIN_OPS.as_slice(); + ops.iter().map(|op| (op.jsonlogic_id, op)).collect() + }); /// Get all builtin operations (for internal use by evaluator) pub(crate) fn get_builtin_ops() -> &'static [BuiltinOp] { - BUILTIN_OPS + BUILTIN_OPS.as_slice() } /// Find a builtin operation by its Scheme identifier @@ -616,6 +605,21 @@ mod tests { Some(val(value)) } + /// Helper to invoke a builtin through the public registry using + /// the canonical erased signature (Vec -> Result). + /// + /// This keeps tests independent of the internal typed helper + /// function signatures while still exercising the adapter layer. + fn call_builtin(name: &str, args: &[Value]) -> Result { + let op = find_scheme_op(name).expect("builtin not found"); + match &op.op_kind { + OpKind::Function(func) => func(args.to_vec()), + OpKind::SpecialForm(_) => { + panic!("expected function builtin in tests, got special form: {name}") + } + } + } + #[test] fn test_builtin_ops_registry() { // Test lookup by both scheme and jsonlogic ids @@ -633,7 +637,7 @@ mod tests { assert!(!add_op.is_special_form()); if let OpKind::Function(func) = &add_op.op_kind { - let result = func(&[val(1), val(2)]).unwrap(); + let result = func(vec![val(1), val(2)]).unwrap(); assert_eq!(result, val(3)); } else { panic!("Expected Function variant"); @@ -692,10 +696,10 @@ mod tests { ); } - /// Macro to create test cases with identifying information for better error messages + /// Macro to create test cases, invoking builtins via the registry. macro_rules! test { - ($expr:expr, $expected:expr) => { - (stringify!($expr), $expr, $expected) + ($name:expr, $args:expr, $expected:expr) => { + ($name, call_builtin($name, $args), $expected) }; } @@ -735,236 +739,240 @@ mod tests { // ================================================================= // Test arithmetic functions - addition - test!(builtin_add(&[]), success(0)), // Identity - test!(builtin_add(&[val(5)]), success(5)), // Single number - test!(builtin_add(&[val(1), val(2), val(3)]), success(6)), // Multiple numbers - test!(builtin_add(&[val(-5), val(10)]), success(5)), // Negative numbers - test!(builtin_add(&[val(0), val(0), val(0)]), success(0)), // Zeros + test!("+", &[], success(0)), // Identity + test!("+", &[val(5)], success(5)), // Single number + test!("+", &[val(1), val(2), val(3)], success(6)), // Multiple numbers + test!("+", &[val(-5), val(10)], success(5)), // Negative numbers + test!("+", &[val(0), val(0), val(0)], success(0)), // Zeros // Test addition error cases - test!(builtin_add(&[val("not a number")]), None), // Invalid type - test!(builtin_add(&[val(1), val(true)]), None), // Mixed types + test!("+", &[val("not a number")], None), // Invalid type + test!("+", &[val(1), val(true)], None), // Mixed types // Test arithmetic functions - subtraction - test!(builtin_sub(&[val(5)]), success(-5)), // Unary minus - test!(builtin_sub(&[val(-5)]), success(5)), // Unary minus of negative - test!(builtin_sub(&[val(10), val(3), val(2)]), success(5)), // Multiple subtraction - test!(builtin_sub(&[val(0), val(5)]), success(-5)), // Zero minus number - test!(builtin_sub(&[val(10), val(0)]), success(10)), // Number minus zero + test!("-", &[val(5)], success(-5)), // Unary minus + test!("-", &[val(-5)], success(5)), // Unary minus of negative + test!("-", &[val(10), val(3), val(2)], success(5)), // Multiple subtraction + test!("-", &[val(0), val(5)], success(-5)), // Zero minus number + test!("-", &[val(10), val(0)], success(10)), // Number minus zero // Test subtraction error cases - test!(builtin_sub(&[]), None), // No arguments - test!(builtin_sub(&[val("not a number")]), None), - test!(builtin_sub(&[val(5), val(false)]), None), + test!("-", &[], None), // No arguments + test!("-", &[val("not a number")], None), + test!("-", &[val(5), val(false)], None), // Test arithmetic functions - multiplication // SCHEME-STRICT: We require at least 1 argument (Scheme R7RS allows 0 args, returns 1) - test!(builtin_mul(&[]), None), // No arguments should error - test!(builtin_mul(&[val(5)]), success(5)), // Single number - test!(builtin_mul(&[val(2), val(3), val(4)]), success(24)), // Multiple numbers - test!(builtin_mul(&[val(-2), val(3)]), success(-6)), // Negative numbers - test!(builtin_mul(&[val(0), val(100)]), success(0)), // Zero multiplication - test!(builtin_mul(&[val(1), val(1), val(1)]), success(1)), // Ones + test!("*", &[], None), // No arguments should error + test!("*", &[val(5)], success(5)), // Single number + test!("*", &[val(2), val(3), val(4)], success(24)), // Multiple numbers + test!("*", &[val(-2), val(3)], success(-6)), // Negative numbers + test!("*", &[val(0), val(100)], success(0)), // Zero multiplication + test!("*", &[val(1), val(1), val(1)], success(1)), // Ones // Test multiplication error cases - test!(builtin_mul(&[val("not a number")]), None), - test!(builtin_mul(&[val(2), nil()]), None), + test!("*", &[val("not a number")], None), + test!("*", &[val(2), nil()], None), // Test comparison functions - greater than - test!(builtin_gt(&[val(7), val(3)]), success(true)), - test!(builtin_gt(&[val(3), val(8)]), success(false)), - test!(builtin_gt(&[val(4), val(4)]), success(false)), // Equal case - test!(builtin_gt(&[val(-1), val(-2)]), success(true)), // Negative numbers + test!(">", &[val(7), val(3)], success(true)), + test!(">", &[val(3), val(8)], success(false)), + test!(">", &[val(4), val(4)], success(false)), // Equal case + test!(">", &[val(-1), val(-2)], success(true)), // Negative numbers // Test chaining behavior: 9 > 6 > 2 should be true since all adjacent pairs satisfy > - test!(builtin_gt(&[val(9), val(6), val(2)]), success(true)), // Chaining true + test!(">", &[val(9), val(6), val(2)], success(true)), // Chaining true // Test chaining that should fail: 9 > 6 > 7 should be false since 6 > 7 is false - test!(builtin_gt(&[val(9), val(6), val(7)]), success(false)), // Chaining false + test!(">", &[val(9), val(6), val(7)], success(false)), // Chaining false // Test comparison error cases (wrong number of args or wrong types) - test!(builtin_gt(&[val(5)]), None), // Too few args - test!(builtin_gt(&[val("a"), val(3)]), None), // Wrong type + test!(">", &[val(5)], None), // Too few args + test!(">", &[val("a"), val(3)], None), // Wrong type // Test comparison functions - greater than or equal - test!(builtin_ge(&[val(8), val(3)]), success(true)), - test!(builtin_ge(&[val(2), val(6)]), success(false)), - test!(builtin_ge(&[val(7), val(7)]), success(true)), // Equal case + test!(">=", &[val(8), val(3)], success(true)), + test!(">=", &[val(2), val(6)], success(false)), + test!(">=", &[val(7), val(7)], success(true)), // Equal case // Test comparison functions - less than - test!(builtin_lt(&[val(2), val(9)]), success(true)), - test!(builtin_lt(&[val(8), val(4)]), success(false)), - test!(builtin_lt(&[val(6), val(6)]), success(false)), // Equal case + test!("<", &[val(2), val(9)], success(true)), + test!("<", &[val(8), val(4)], success(false)), + test!("<", &[val(6), val(6)], success(false)), // Equal case // Test numeric comparison chaining: 1 < 2 < 3 (all adjacent pairs satisfy <) - test!(builtin_lt(&[val(1), val(2), val(3)]), success(true)), // Chaining true + test!("<", &[val(1), val(2), val(3)], success(true)), // Chaining true // Test chaining that should fail: 1 < 3 but not 3 < 2 - test!(builtin_lt(&[val(1), val(3), val(2)]), success(false)), // Chaining false + test!("<", &[val(1), val(3), val(2)], success(false)), // Chaining false // Test comparison functions - less than or equal - test!(builtin_le(&[val(4), val(9)]), success(true)), - test!(builtin_le(&[val(8), val(2)]), success(false)), - test!(builtin_le(&[val(3), val(3)]), success(true)), // Equal case + test!("<=", &[val(4), val(9)], success(true)), + test!("<=", &[val(8), val(2)], success(false)), + test!("<=", &[val(3), val(3)], success(true)), // Equal case // Test numeric equality - test!(builtin_eq(&[val(12), val(12)]), success(true)), - test!(builtin_eq(&[val(8), val(3)]), success(false)), - test!(builtin_eq(&[val(0), val(0)]), success(true)), - test!(builtin_eq(&[val(-1), val(-1)]), success(true)), - test!(builtin_eq(&[val(7), val(7), val(7)]), success(true)), // 7 = 7 = 7 (all equal) - test!(builtin_eq(&[val(9), val(9), val(4)]), success(false)), // 9 = 9 but not 9 = 4 + test!("=", &[val(12), val(12)], success(true)), + test!("=", &[val(8), val(3)], success(false)), + test!("=", &[val(0), val(0)], success(true)), + test!("=", &[val(-1), val(-1)], success(true)), + test!("=", &[val(7), val(7), val(7)], success(true)), // 7 = 7 = 7 (all equal) + test!("=", &[val(9), val(9), val(4)], success(false)), // 9 = 9 but not 9 = 4 // Test structural equality (equal?) - test!(builtin_equal(&[val(11), val(11)]), success(true)), - test!(builtin_equal(&[val(15), val(3)]), success(false)), - test!(builtin_equal(&[val("hello"), val("hello")]), success(true)), - test!(builtin_equal(&[val("hello"), val("world")]), success(false)), - test!(builtin_equal(&[val(true), val(true)]), success(true)), - test!(builtin_equal(&[val(true), val(false)]), success(false)), - test!(builtin_equal(&[nil(), nil()]), success(true)), - test!(builtin_equal(&[val([1]), val([1])]), success(true)), - test!(builtin_equal(&[val(5), val("5")]), None), // Different types - now rejected + test!("equal?", &[val(11), val(11)], success(true)), + test!("equal?", &[val(15), val(3)], success(false)), + test!("equal?", &[val("hello"), val("hello")], success(true)), + test!("equal?", &[val("hello"), val("world")], success(false)), + test!("equal?", &[val(true), val(true)], success(true)), + test!("equal?", &[val(true), val(false)], success(false)), + test!("equal?", &[nil(), nil()], success(true)), + test!("equal?", &[val([1]), val([1])], success(true)), + test!("equal?", &[val(5), val("5")], None), // Different types - now rejected // Test equal? error cases (structural equality requires exactly 2 args) - test!(builtin_equal(&[val(5)]), None), // Too few args - test!(builtin_equal(&[val(5), val(3), val(1)]), None), // Too many args + test!("equal?", &[val(5)], None), // Too few args + test!("equal?", &[val(5), val(3), val(1)], None), // Too many args // Test logical functions - not - test!(builtin_not(&[val(true)]), success(false)), - test!(builtin_not(&[val(false)]), success(true)), + test!("not", &[val(true)], success(false)), + test!("not", &[val(false)], success(true)), // Test not error cases - test!(builtin_not(&[]), None), // No args - test!(builtin_not(&[val(true), val(false)]), None), // Too many args - test!(builtin_not(&[val(1)]), None), // Wrong type - test!(builtin_not(&[val("true")]), None), // Wrong type + test!("not", &[], None), // No args + test!("not", &[val(true), val(false)], None), // Too many args + test!("not", &[val(1)], None), // Wrong type + test!("not", &[val("true")], None), // Wrong type // Test list functions - car - test!(builtin_car(&[val([1, 2, 3])]), success(1)), // First element - test!(builtin_car(&[val(["only"])]), success("only")), // Single element - test!(builtin_car(&[val([val([1]), val(2)])]), success([1])), // Nested list + test!("car", &[val([1, 2, 3])], success(1)), // First element + test!("car", &[val(["only"])], success("only")), // Single element + test!("car", &[val([val([1]), val(2)])], success([1])), // Nested list // Test car error cases - test!(builtin_car(&[]), None), // No args - test!(builtin_car(&[int_list.clone(), int_list.clone()]), None), // Too many args - test!(builtin_car(&[nil()]), None), // Empty list - test!(builtin_car(&[val(42)]), None), // Not a list - test!(builtin_car(&[val("not a list")]), None), // Not a list + test!("car", &[], None), // No args + test!("car", &[int_list.clone(), int_list.clone()], None), // Too many args + test!("car", &[nil()], None), // Empty list + test!("car", &[val(42)], None), // Not a list + test!("car", &[val("not a list")], None), // Not a list // Test list functions - cdr - test!(builtin_cdr(&[val([1, 2, 3])]), success([2, 3])), // Rest of list - test!(builtin_cdr(&[val(["only"])]), Some(nil())), // Single element -> empty - test!(builtin_cdr(&[val([1, 2])]), success([2])), // Two elements + test!("cdr", &[val([1, 2, 3])], success([2, 3])), // Rest of list + test!("cdr", &[val(["only"])], Some(nil())), // Single element -> empty + test!("cdr", &[val([1, 2])], success([2])), // Two elements // Test cdr error cases - test!(builtin_cdr(&[]), None), // No args - test!(builtin_cdr(&[int_list.clone(), int_list]), None), // Too many args - test!(builtin_cdr(&[nil()]), None), // Empty list - test!(builtin_cdr(&[val(true)]), None), // Not a list + test!("cdr", &[], None), // No args + test!("cdr", &[int_list.clone(), int_list], None), // Too many args + test!("cdr", &[nil()], None), // Empty list + test!("cdr", &[val(true)], None), // Not a list // Test list functions - cons - test!(builtin_cons(&[val(0), val([1, 2])]), success([0, 1, 2])), // Prepend to list - test!(builtin_cons(&[val("first"), nil()]), success(["first"])), // Cons to empty - test!( - builtin_cons(&[val([1]), val([2])]), - success([val([1]), val(2)]) - ), // Nested cons + test!("cons", &[val(0), val([1, 2])], success([0, 1, 2])), // Prepend to list + test!("cons", &[val("first"), nil()], success(["first"])), // Cons to empty + test!("cons", &[val([1]), val([2])], success([val([1]), val(2)])), // Nested cons // Test cons error cases - test!(builtin_cons(&[]), None), // No args - test!(builtin_cons(&[val(1)]), None), // Too few args - test!(builtin_cons(&[val(1), val(2), val(3)]), None), // Too many args - test!(builtin_cons(&[val(1), val(2)]), None), // Second arg not a list - test!(builtin_cons(&[val(1), val("not a list")]), None), // Second arg not a list + test!("cons", &[], None), // No args + test!("cons", &[val(1)], None), // Too few args + test!("cons", &[val(1), val(2), val(3)], None), // Too many args + test!("cons", &[val(1), val(2)], None), // Second arg not a list + test!("cons", &[val(1), val("not a list")], None), // Second arg not a list // Test list functions - list - test!(builtin_list(&[]), Some(nil())), // Empty list - test!(builtin_list(&[val(1)]), success([1])), // Single element + test!("list", &[], Some(nil())), // Empty list + test!("list", &[val(1)], success([1])), // Single element test!( - builtin_list(&[val(1), val("hello"), val(true)]), + "list", + &[val(1), val("hello"), val(true)], success([val(1), val("hello"), val(true)]) ), // Mixed types - test!( - builtin_list(&[val([1]), val(2)]), - success([val([1]), val(2)]) - ), // Nested lists + test!("list", &[val([1]), val(2)], success([val([1]), val(2)])), // Nested lists // Test null? function - test!(builtin_null(&[nil()]), success(true)), // Empty list is nil - test!(builtin_null(&[val(42)]), success(false)), // Number is not nil - test!(builtin_null(&[val("")]), success(false)), // Empty string is not nil - test!(builtin_null(&[val(false)]), success(false)), // False is not nil - test!(builtin_null(&[val([1])]), success(false)), // Non-empty list is not nil + test!("null?", &[nil()], success(true)), // Empty list is nil + test!("null?", &[val(42)], success(false)), // Number is not nil + test!("null?", &[val("")], success(false)), // Empty string is not nil + test!("null?", &[val(false)], success(false)), // False is not nil + test!("null?", &[val([1])], success(false)), // Non-empty list is not nil // Test null? error cases - test!(builtin_null(&[]), None), // No args - test!(builtin_null(&[val(1), val(2)]), None), // Too many args + test!("null?", &[], None), // No args + test!("null?", &[val(1), val(2)], None), // Too many args // Test error function - test!(builtin_error(&[]), None), // No args - should produce generic error - test!(builtin_error(&[val("test error")]), None), // String message - test!(builtin_error(&[val(42)]), None), // Number message - test!(builtin_error(&[val(true)]), None), // Bool message - test!( - builtin_error(&[val("Error:"), val("Something went wrong")]), - None - ), // Multiple args + test!("error", &[], None), // No args - should produce generic error + test!("error", &[val("test error")], None), // String message + test!("error", &[val(42)], None), // Number message + test!("error", &[val(true)], None), // Bool message + test!("error", &[val("Error:"), val("Something went wrong")], None), // Multiple args // ================================================================= // ARITHMETIC EDGE CASES // ================================================================= // Integer overflow cases (should fail) - test!(builtin_add(&[val(NumberType::MAX), val(1)]), None), // Addition overflow - test!(builtin_mul(&[val(NumberType::MAX), val(2)]), None), // Multiplication overflow - test!(builtin_sub(&[val(NumberType::MIN)]), None), // Negation overflow - test!(builtin_sub(&[val(NumberType::MIN), val(1)]), None), // Subtraction overflow + test!("+", &[val(NumberType::MAX), val(1)], None), // Addition overflow + test!("*", &[val(NumberType::MAX), val(2)], None), // Multiplication overflow + test!("-", &[val(NumberType::MIN)], None), // Negation overflow + test!("-", &[val(NumberType::MIN), val(1)], None), // Subtraction overflow // Boundary values (should succeed) test!( - builtin_add(&[val(NumberType::MAX), val(0)]), + "+", + &[val(NumberType::MAX), val(0)], success(NumberType::MAX) ), test!( - builtin_sub(&[val(NumberType::MIN), val(0)]), + "-", + &[val(NumberType::MIN), val(0)], success(NumberType::MIN) ), test!( - builtin_mul(&[val(NumberType::MAX), val(1)]), + "*", + &[val(NumberType::MAX), val(1)], success(NumberType::MAX) ), - test!(builtin_mul(&[val(0), val(NumberType::MAX)]), success(0)), + test!("*", &[val(0), val(NumberType::MAX)], success(0)), // Operations with zero - test!(builtin_add(&[val(0)]), success(0)), - test!(builtin_sub(&[val(0)]), success(0)), - test!(builtin_mul(&[val(0)]), success(0)), + test!("+", &[val(0)], success(0)), + test!("-", &[val(0)], success(0)), + test!("*", &[val(0)], success(0)), // Large chain operations - test!(builtin_add(&many_ones), success(100)), - test!(builtin_mul(&many_ones), success(1)), + test!("+", &many_ones, success(100)), + test!("*", &many_ones, success(1)), // ================================================================= // COMPARISON EDGE CASES // ================================================================= // Boundary comparisons test!( - builtin_gt(&[val(NumberType::MAX), val(NumberType::MIN)]), + ">", + &[val(NumberType::MAX), val(NumberType::MIN)], success(true) ), test!( - builtin_lt(&[val(NumberType::MIN), val(NumberType::MAX)]), + "<", + &[val(NumberType::MIN), val(NumberType::MAX)], success(true) ), test!( - builtin_ge(&[val(NumberType::MAX), val(NumberType::MAX)]), + ">=", + &[val(NumberType::MAX), val(NumberType::MAX)], success(true) ), test!( - builtin_le(&[val(NumberType::MIN), val(NumberType::MIN)]), + "<=", + &[val(NumberType::MIN), val(NumberType::MIN)], success(true) ), // Long chain comparisons test!( - builtin_lt(&[val(-5), val(-2), val(0), val(3), val(10)]), + "<", + &[val(-5), val(-2), val(0), val(3), val(10)], success(true) ), test!( - builtin_gt(&[val(10), val(5), val(0), val(-3), val(-8)]), + ">", + &[val(10), val(5), val(0), val(-3), val(-8)], success(true) ), - test!(builtin_lt(&[val(1), val(2), val(1)]), success(false)), // 2 > 1 fails + test!("<", &[val(1), val(2), val(1)], success(false)), // 2 > 1 fails // Numeric equality with many values - test!(builtin_eq(&all_fives), success(true)), - test!(builtin_eq(&mostly_fives), success(false)), + test!("=", &all_fives, success(true)), + test!("=", &mostly_fives, success(false)), // ================================================================= // LIST OPERATIONS EDGE CASES // ================================================================= // Deeply nested lists - test!(builtin_car(&[nested]), success([val([1])])), + test!("car", &[nested], success([val([1])])), // Mixed type lists operations - test!(builtin_car(std::slice::from_ref(&mixed)), success(1)), + test!("car", std::slice::from_ref(&mixed), success(1)), test!( - builtin_cdr(std::slice::from_ref(&mixed)), + "cdr", + std::slice::from_ref(&mixed), success([val("hello"), val(true), nil()]) ), // Cons with various types test!( - builtin_cons(&[val(true), val([val(1), val(2)])]), + "cons", + &[val(true), val([val(1), val(2)])], success([val(true), val(1), val(2)]) ), // List creation with many elements test!( - builtin_list(&many_elements), + "list", + &many_elements, success((0..50).map(val).collect::>()) ), // ================================================================= @@ -972,64 +980,66 @@ mod tests { // ================================================================= // Basic string concatenation - test!(builtin_string_append(&[]), success("")), - test!(builtin_string_append(&[val("hello")]), success("hello")), + test!("string-append", &[], success("")), + test!("string-append", &[val("hello")], success("hello")), test!( - builtin_string_append(&[val("hello"), val(" "), val("world")]), + "string-append", + &[val("hello"), val(" "), val("world")], success("hello world") ), test!( - builtin_string_append(&[val(""), val("test"), val("")]), + "string-append", + &[val(""), val("test"), val("")], success("test") ), // Error cases - non-string arguments - test!(builtin_string_append(&[val(42)]), None), - test!(builtin_string_append(&[val("hello"), val(123)]), None), - test!(builtin_string_append(&[val(true), val("world")]), None), + test!("string-append", &[val(42)], None), + test!("string-append", &[val("hello"), val(123)], None), + test!("string-append", &[val(true), val("world")], None), // ================================================================= // MATH OPERATIONS - MAX/MIN // ================================================================= // Basic max operations - test!(builtin_max(&[val(5)]), success(5)), - test!(builtin_max(&[val(1), val(2), val(3)]), success(3)), - test!(builtin_max(&[val(3), val(1), val(2)]), success(3)), - test!(builtin_max(&[val(-5), val(-1), val(-10)]), success(-1)), + test!("max", &[val(5)], success(5)), + test!("max", &[val(1), val(2), val(3)], success(3)), + test!("max", &[val(3), val(1), val(2)], success(3)), + test!("max", &[val(-5), val(-1), val(-10)], success(-1)), // Basic min operations - test!(builtin_min(&[val(5)]), success(5)), - test!(builtin_min(&[val(1), val(2), val(3)]), success(1)), - test!(builtin_min(&[val(3), val(1), val(2)]), success(1)), - test!(builtin_min(&[val(-5), val(-1), val(-10)]), success(-10)), + test!("min", &[val(5)], success(5)), + test!("min", &[val(1), val(2), val(3)], success(1)), + test!("min", &[val(3), val(1), val(2)], success(1)), + test!("min", &[val(-5), val(-1), val(-10)], success(-10)), // Error cases - no arguments - test!(builtin_max(&[]), None), - test!(builtin_min(&[]), None), + test!("max", &[], None), + test!("min", &[], None), // Error cases - non-number arguments - test!(builtin_max(&[val("hello")]), None), - test!(builtin_min(&[val(true)]), None), - test!(builtin_max(&[val(1), val("hello")]), None), - test!(builtin_min(&[val(1), val(true)]), None), + test!("max", &[val("hello")], None), + test!("min", &[val(true)], None), + test!("max", &[val(1), val("hello")], None), + test!("min", &[val(1), val(true)], None), // ================================================================= // EQUALITY STRICT TYPING - OVERRIDE BASIC EQUAL TESTS // ================================================================= // Type coercion rejection (these should fail) - test!(builtin_equal(&[val(1), val("1")]), None), - test!(builtin_equal(&[val(0), val(false)]), None), - test!(builtin_equal(&[val(true), val(1)]), None), - test!(builtin_equal(&[val(""), nil()]), None), - test!(builtin_equal(&[val(Vec::::new()), val(false)]), None), + test!("equal?", &[val(1), val("1")], None), + test!("equal?", &[val(0), val(false)], None), + test!("equal?", &[val(true), val(1)], None), + test!("equal?", &[val(""), nil()], None), + test!("equal?", &[val(Vec::::new()), val(false)], None), // Complex same-type structures - test!(builtin_equal(&[complex1.clone(), complex2]), success(true)), - test!(builtin_equal(&[complex1, complex3]), success(false)), + test!("equal?", &[complex1.clone(), complex2], success(true)), + test!("equal?", &[complex1, complex3], success(false)), // ================================================================= // LOGICAL OPERATIONS STRICT - ADDITIONAL ERROR CASES // ================================================================= // Non-boolean inputs should fail - test!(builtin_not(&[val(0)]), None), - test!(builtin_not(&[val("")]), None), - test!(builtin_not(&[nil()]), None), - test!(builtin_not(&[val("false")]), None), + test!("not", &[val(0)], None), + test!("not", &[val("")], None), + test!("not", &[nil()], None), + test!("not", &[val("false")], None), ]; for (test_expr, result, expected) in test_cases { @@ -1064,7 +1074,7 @@ mod tests { ]; for (args, expected_msg) in test_cases { - match builtin_error(&args).unwrap_err() { + match call_builtin("error", &args).unwrap_err() { Error::EvalError(msg) => { assert_eq!(msg, expected_msg, "Failed for args: {args:?}"); } diff --git a/src/evaluator.rs b/src/evaluator.rs index a48a649..a13d531 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1,7 +1,12 @@ +use self::intooperation::{IntoOperation, IntoVariadicOperation}; use crate::Error; use crate::MAX_EVAL_DEPTH; use crate::ast::Value; -use crate::builtinops::get_builtin_ops; +use crate::builtinops::{Arity, get_builtin_ops}; +use std::sync::Arc; + +pub(crate) mod intooperation; +pub use self::intooperation::{BoolIter, NumIter, StringIter, ValueIter}; use std::collections::HashMap; /// Environment for variable bindings @@ -36,37 +41,133 @@ impl Environment { .or_else(|| self.parent.as_ref().and_then(|parent| parent.get(name))) } - /// Register a custom builtin function in the environment for use by Scheme/JSONLogic. + /// Register a strongly-typed Rust function as a builtin operation using + /// automatic argument extraction and result conversion. + /// + /// This allows writing natural Rust functions like: /// - /// # Arguments - /// * `name` - The name by which the function can be called - /// * `func` - A function pointer that takes a slice of Values and returns a Result + /// ```rust,ignore + /// use rulesxp::{Error, evaluator}; /// - /// # Example + /// // Infallible builtin: returns a bare i64 + /// fn add(a: i64, b: i64) -> i64 { + /// a + b + /// } + /// let mut env = evaluator::create_global_env(); + /// env.register_builtin_operation::<(i64, i64)>("add", add); + /// // Now (+ 2 3) and (add 2 3) both work if + also registered /// ``` - /// use rulesxp::evaluator::{Environment, create_global_env}; - /// use rulesxp::ast::Value; - /// use rulesxp::Error; /// - /// fn my_custom_function(args: &[Value]) -> Result { - /// println!("Custom function called with {} args", args.len()); - /// Ok(Value::Unspecified) + /// Builtins that may fail return `Result` where `T` is either + /// a Value or a type convertible into Value, and encode + /// their own error messages: + /// + /// ```rust,ignore + /// use rulesxp::{Error, evaluator}; + /// + /// fn safe_div(a: i64, b: i64) -> Result { + /// if b == 0 { + /// Err(Error::EvalError("division by zero".into())) + /// } else { + /// Ok(a / b) + /// } /// } /// - /// let mut env = create_global_env(); - /// env.register_builtin_function("my-func", my_custom_function); - /// // Now (my-func) can be called from evaluated expressions + /// let mut env = evaluator::create_global_env(); + /// env.register_builtin_operation::<(i64, i64)>("safe-div", safe_div); /// ``` - pub fn register_builtin_function( + /// + /// Supported parameter types (initial set): + /// - `i64` (number) + /// - `bool` (boolean) + /// - `&str` (borrowed string slices) + /// - `Value` (owned access to the raw AST value) + /// - `ValueIter<'_>` (iterates over elements of a list argument as `&Value`) + /// - `NumIter<'_>` (iterates over numeric elements of a list argument) + /// - `BoolIter<'_>` (iterates over boolean elements of a list argument) + /// - `StringIter<'_>` (iterates over string elements of a list argument) + /// + /// Additional scalar parameter types can be supported by adding + /// `impl TryInto for Value`. + /// + /// More advanced list-style and variadic behavior (e.g. rest + /// parameters spanning multiple arguments) can be expressed using + /// the iterator-based APIs described on + /// [`Environment::register_variadic_builtin_operation`]. + /// + /// Supported return types: + /// - `Result` + /// - `Result` where `T: Into` (for example + /// `i64`, `bool`, `&str`, or arrays/vectors of these types) + /// - bare `T` where `T: Into` (for infallible helpers, + /// automatically wrapped as `Ok(T)`) + /// + /// If you need true rest-parameter / variadic behavior (functions + /// that see all arguments via iterators over the argument tail), + /// use [`Environment::register_variadic_builtin_operation`] instead. + /// + /// Arity is enforced automatically. Conversion errors yield + /// `TypeError`, and builtin errors are surfaced directly as + /// `Error` values. + #[expect(private_bounds)] // IntoOperation is an internal adapter trait modeling an input function + pub fn register_builtin_operation( &mut self, name: &str, - func: fn(&[Value]) -> Result, + func: impl IntoOperation + 'static, ) { + let wrapped = func.into_operation(); self.bindings.insert( name.to_string(), Value::BuiltinFunction { id: name.to_string(), - func, + func: wrapped, + }, + ); + } + + /// Register a variadic builtin operation with explicit arity metadata. + /// + /// This is intended for functions whose Rust signature includes a + /// "rest" parameter, expressed using iterator types such as + /// [`ValueIter`] and [`NumIter`]. + /// + /// Examples: + /// - rest of all arguments as values: + /// `fn(ValueIter<'_>) -> Result` with + /// `Args = (ValueIter<'static>,)` + /// - numeric tail: + /// `fn(NumIter<'_>) -> Result` with + /// `Args = (NumIter<'static>,)` + /// - fixed prefix plus numeric tail: + /// `fn(i64, NumIter<'_>) -> Result` with + /// `Args = (i64, NumIter<'static>)` + /// + /// Fixed-arity functions should use + /// [`Environment::register_builtin_operation`] instead. + /// + /// The provided [`Arity`] is used to validate the total number of + /// arguments at call time, since minimum/maximum argument counts for + /// variadic operations are not always derivable from the Rust type + /// signature alone. + #[expect(private_bounds)] // IntoVariadicOperation is an internal trait modeling an input function + pub fn register_variadic_builtin_operation( + &mut self, + name: &str, + arity: Arity, + func: impl IntoVariadicOperation + 'static, + ) { + let inner = func.into_variadic_operation(); + let arity_for_closure = arity; + let wrapped = std::sync::Arc::new(move |args: Vec| { + arity_for_closure.validate(args.len())?; + inner(args) + }); + + self.bindings.insert( + name.to_string(), + Value::BuiltinFunction { + id: name.to_string(), + func: wrapped, }, ); } @@ -137,7 +238,7 @@ fn eval_with_depth_tracking( // Evaluate all arguments using helper function with depth tracking let evaluated_args = eval_args(args, env, depth)?; // Apply the function (arity already validated at parse time) - f(&evaluated_args) + f(evaluated_args) } OpKind::SpecialForm(special_form) => { // Special forms are syntax structures handled here after being converted @@ -203,7 +304,7 @@ fn eval_list(elements: &[Value], env: &mut Environment, depth: usize) -> Result< // Apply the function match &func { // Dynamic function calls - Value::BuiltinFunction { func: f, .. } => f(&args), + Value::BuiltinFunction { func, .. } => func(args), Value::Function { params, body, @@ -413,7 +514,7 @@ pub fn create_global_env() -> Environment { builtin_op.scheme_id.to_owned(), Value::BuiltinFunction { id: builtin_op.scheme_id.to_owned(), - func: *func, + func: Arc::clone(func), }, ); } @@ -426,7 +527,9 @@ pub fn create_global_env() -> Environment { #[expect(clippy::unwrap_used)] // test code OK mod tests { use super::*; + use crate::Error; use crate::ast::{nil, sym, val}; + use crate::evaluator::{NumIter, ValueIter}; use crate::scheme::parse_scheme; /// Test result variants for comprehensive testing @@ -435,6 +538,7 @@ mod tests { EvalResult(Value), // Evaluation should succeed with this value SpecificError(&'static str), // Evaluation should fail with error containing this string Error, // Evaluation should fail (any error) + ArityError, // Evaluation should fail specifically with an ArityError } use TestResult::*; @@ -489,6 +593,14 @@ mod tests { } } + (Err(crate::Error::ArityError { .. }), ArityError) => {} // Expected specific arity error + (Ok(actual), ArityError) => { + panic!("{test_id}: expected ArityError, got successful result {actual:?}"); + } + (Err(err), ArityError) => { + panic!("{test_id}: expected ArityError, got different error {err:?}"); + } + (Err(_), Error) => {} // Expected generic error (Err(e), SpecificError(expected_text)) => { let error_msg = format!("{e}"); @@ -518,6 +630,129 @@ mod tests { } } + /// Run tests in a caller-provided environment (e.g., with custom builtins registered). + fn run_tests_in_specific_environment( + env: &mut Environment, + test_cases: Vec<(&str, TestResult)>, + ) { + for (i, (input, expected)) in test_cases.iter().enumerate() { + let test_id = format!("#custom-{}", i + 1); + execute_test_case(input, expected, env, &test_id); + } + } + + #[test] + fn test_custom_builtin_operations() { + // Custom builtins exercise the adapter layer: fixed arity, zero-arg, + // list iterators, explicit arity metadata, and variadic rest-parameters. + fn add(a: i64, b: i64) -> i64 { + a + b + } + + fn forty_two() -> i64 { + 42 + } + + fn safe_div(a: i64, b: i64) -> Result { + if b == 0 { + Err(Error::EvalError("division by zero".into())) + } else { + Ok(a / b) + } + } + + fn sum_list(nums: NumIter<'_>) -> Result { + Ok(nums.sum()) + } + + fn first_and_rest_count(mut args: ValueIter<'_>) -> Result, Error> { + let first = match args.next() { + Some(Value::Number(n)) => *n, + Some(_) => return Err(Error::TypeError("first argument must be a number".into())), + None => return Err(Error::arity_error(1, 0)), + }; + + let rest_count = args.count() as i64; + + Ok(vec![first, rest_count]) + } + + fn count_numbers(args: ValueIter<'_>) -> Result { + let count = args.filter(|v| matches!(v, Value::Number(_))).count() as i64; + Ok(count) + } + + fn sum_all(nums: NumIter<'_>) -> Result { + Ok(nums.sum::()) + } + + fn weighted_sum(weight: i64, nums: NumIter<'_>) -> Result { + Ok(weight * nums.sum::()) + } + + fn sum_all_min1(nums: NumIter<'_>) -> Result { + Ok(nums.sum::()) + } + + let mut env = create_global_env(); + + // Fixed-arity and zero-arg builtins. + env.register_builtin_operation::<(i64, i64)>("add2", add); + env.register_builtin_operation::<()>("forty-two", forty_two); + env.register_builtin_operation::<(i64, i64)>("safe-div", safe_div); + + // Iterator-based list and variadic builtins. + env.register_builtin_operation::<(NumIter<'static>,)>("sum-list", sum_list); + env.register_variadic_builtin_operation::<(ValueIter<'static>,)>( + "first-rest-count", + Arity::AtLeast(1), + first_and_rest_count, + ); + env.register_variadic_builtin_operation::<(ValueIter<'static>,)>( + "count-numbers", + Arity::AtLeast(0), + count_numbers, + ); + env.register_variadic_builtin_operation::<(NumIter<'static>,)>( + "sum-varargs-all", + Arity::AtLeast(0), + sum_all, + ); + env.register_variadic_builtin_operation::<(i64, NumIter<'static>)>( + "weighted-sum", + Arity::AtLeast(1), + weighted_sum, + ); + env.register_variadic_builtin_operation::<(NumIter<'static>,)>( + "sum-all-min1", + Arity::AtLeast(1), + sum_all_min1, + ); + + let test_cases = vec![ + // Fixed-arity and zero-arg builtins. + ("(add2 7 5)", success(12)), + ("(forty-two)", success(42)), + ("(safe-div 6 3)", success(2)), + // Error case: division by zero surfaces as EvalError containing the message. + ("(safe-div 1 0)", SpecificError("division by zero")), + // Iterator-based list and variadic builtins. + ("(sum-list (list 1 2 3 4))", success(10)), + ("(first-rest-count 42 \"x\" #t 7)", success([42, 3])), + ("(count-numbers 1 \"x\" 2 #t 3)", success(3)), + ("(sum-varargs-all 1 2 3 4)", success(10)), + ("(weighted-sum 2 1 2 3)", success(12)), + // Explicit arity checking for variadic builtin. + ("(sum-all-min1 1 2 3)", success(6)), + ("(sum-all-min1)", ArityError), + // Dynamic higher-order use of a builtin comparison: pass `>` as a value. + ("((lambda (op a b c) (op a b c)) > 9 6 2)", success(true)), + ("((lambda (op a b c) (op a b c)) > 9 6 7)", success(false)), + ]; + + run_tests_in_specific_environment(&mut env, test_cases); + } + #[test] #[expect(clippy::too_many_lines)] // Comprehensive test coverage is intentionally thorough fn test_comprehensive_operations_data_driven() { @@ -770,7 +1005,7 @@ mod tests { // Test undefined variable errors ("undefined-var", Error), // Test type errors propagate through calls - ("(not 42)", SpecificError("boolean argument")), // Type error with specific message + ("(not 42)", SpecificError("expected boolean")), // Type error with specific message ("(car \"not-a-list\")", Error), // Type error // Test errors in nested expressions ("(+ 1 (car \"not-a-list\"))", Error), @@ -793,10 +1028,7 @@ mod tests { // throughout the chain. If set! were supported this design would need revisiting ("(set! x 42)", SpecificError("Unbound variable: set!")), // Unsupported special forms appear as unbound variables // Type errors - ( - "(+ 1 \"hello\")", - SpecificError("Type error: + requires numbers"), - ), + ("(+ 1 \"hello\")", SpecificError("expected number")), ]; run_comprehensive_tests(test_cases); diff --git a/src/evaluator/intooperation.rs b/src/evaluator/intooperation.rs new file mode 100644 index 0000000..6166f71 --- /dev/null +++ b/src/evaluator/intooperation.rs @@ -0,0 +1,496 @@ +use crate::Error; +use crate::ast::Value; +use std::iter::FusedIterator; +use std::marker::PhantomData; +use std::sync::Arc; + +// NOTE: This module is internal plumbing for the evaluator. +// It defines the adapter layer that turns strongly-typed Rust +// functions into the erased `OperationFn` used at runtime. +// +// External users should interact with `Environment` and the +// registration APIs in `evaluator.rs`; this module is kept +// crate-private but retains rich comments for maintainers. + +/// Canonical erased builtin function type used by the evaluator. +/// +/// Builtins receive ownership of their argument vector, enabling +/// implementations that consume or rearrange arguments if desired. +pub(crate) type OperationFn = dyn Fn(Vec) -> Result + Send + Sync; + +// ===================================================================== +// Internal machinery for fixed-arity argument conversion +// +// This module defines `FromParam`, which is used by the fixed-arity +// adapters to turn AST `Value` nodes into strongly-typed Rust +// parameters. All conversions are implemented here so that the +// supported parameter types are easy to audit. +// ===================================================================== + +/// Core trait used by the fixed-arity adapters to turn `Value` nodes +/// into strongly-typed parameters. +/// +/// The associated `Param<'a>` type is the parameter type as seen by +/// the builtin for a given lifetime of the local `Value` slots used +/// during argument conversion. +pub(crate) trait FromParam { + /// The parameter type as seen by the builtin for a given lifetime + /// of the underlying AST values. + type Param<'a>; + + /// Convert a single AST argument into this parameter type. + /// + /// Implementations may either borrow from the provided `Value` + /// (for types such as `&str` and the borrowed iterators), or + /// consume it by value (for `Value` itself). + fn from_arg<'a>(value: &'a mut Value) -> Result, Error>; +} + +impl FromParam for Value { + type Param<'a> = Value; + + fn from_arg<'a>(value: &'a mut Value) -> Result, Error> { + // Move the `Value` out so that builtin functions can consume + // owned payloads (such as strings or lists) without cloning. + Ok(std::mem::replace(value, Value::Unspecified)) + } +} + +// Blanket implementation for by-value primitive parameters that can be +// obtained from a `Value` via the standard `TryInto` trait. This covers +// types such as `i64` and `bool` for which we provide +// `impl TryInto for Value` in `ast.rs`. +impl FromParam for T +where + Value: std::convert::TryInto, +{ + type Param<'a> = T; + + fn from_arg<'a>(value: &'a mut Value) -> Result, Error> { + let owned = std::mem::replace(value, Value::Unspecified); + >::try_into(owned) + } +} + +impl FromParam for &str { + type Param<'a> = &'a str; + + fn from_arg<'a>(value: &'a mut Value) -> Result, Error> { + if let Value::String(s) = value { + Ok(s.as_str()) + } else { + Err(Error::TypeError("expected string".into())) + } + } +} + +// No slice-based `FromParam` implementations are provided in the +// iterator-based design. If a builtin needs to work with lists or +// rest-style arguments it should use the iterator-based parameters +// defined below instead. + +// ===================================================================== +// FromParam support for iterator parameters (list arguments) +// ===================================================================== + +impl<'b, K> FromParam for TypedValueIter<'b, K> +where + K: ValueElementKind, +{ + type Param<'a> = TypedValueIter<'a, K>; + + fn from_arg<'a>(value: &'a mut Value) -> Result, Error> { + if let Value::List(items) = value { + TypedValueIter::::new(items.as_slice()) + } else { + Err(Error::TypeError("expected list".into())) + } + } +} + +// ===================================================================== +// Generic typed iterator built on top of the standard slice iterator +// ===================================================================== + +/// Marker trait describing how to view a `Value` slice as a typed +/// iterator. Implementations perform any necessary upfront validation +/// and map each `Value` to the element type. +#[doc(hidden)] +pub trait ValueElementKind { + type Item<'a>; + + fn precheck(slice: &[Value]) -> Result<(), Error>; + fn project<'a>(v: &'a Value) -> Self::Item<'a>; +} + +/// Generic iterator over a list of `Value`s, parameterized by a +/// [`ValueElementKind`] that determines the element type and +/// validation. +#[doc(hidden)] +pub struct TypedValueIter<'a, K: ValueElementKind> { + inner: std::slice::Iter<'a, Value>, + _marker: PhantomData, +} + +impl<'a, K> TypedValueIter<'a, K> +where + K: ValueElementKind, +{ + pub(crate) fn new(values: &'a [Value]) -> Result { + K::precheck(values)?; + Ok(TypedValueIter { + inner: values.iter(), + _marker: PhantomData, + }) + } +} + +impl<'a, K> Iterator for TypedValueIter<'a, K> +where + K: ValueElementKind, +{ + type Item = K::Item<'a>; + + fn next(&mut self) -> Option { + let v = self.inner.next()?; + Some(K::project(v)) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl<'a, K> ExactSizeIterator for TypedValueIter<'a, K> where K: ValueElementKind {} +impl<'a, K> FusedIterator for TypedValueIter<'a, K> where K: ValueElementKind {} + +// Concrete element kinds and their iterator aliases + +/// Element kind that views each `Value` as a borrowed reference, +/// used for iterating over raw AST values. +#[doc(hidden)] +pub struct ValueKind; + +impl ValueElementKind for ValueKind { + type Item<'a> = &'a Value; + + fn precheck(_slice: &[Value]) -> Result<(), Error> { + Ok(()) + } + + fn project<'a>(v: &'a Value) -> Self::Item<'a> { + v + } +} + +#[doc(hidden)] +pub struct NumberKind; + +impl ValueElementKind for NumberKind { + type Item<'a> = i64; + + fn precheck(slice: &[Value]) -> Result<(), Error> { + for v in slice { + if !matches!(v, Value::Number(_)) { + return Err(Error::TypeError("expected number".into())); + } + } + Ok(()) + } + + fn project<'a>(v: &'a Value) -> Self::Item<'a> { + if let Value::Number(n) = v { + *n + } else { + debug_assert!(false, "NumberKind::project saw non-number after precheck"); + unreachable!("NumberKind invariant violated") + } + } +} + +#[doc(hidden)] +pub struct BoolKind; + +impl ValueElementKind for BoolKind { + type Item<'a> = bool; + + fn precheck(slice: &[Value]) -> Result<(), Error> { + for v in slice { + if !matches!(v, Value::Bool(_)) { + return Err(Error::TypeError("expected boolean".into())); + } + } + Ok(()) + } + + fn project<'a>(v: &'a Value) -> Self::Item<'a> { + if let Value::Bool(b) = v { + *b + } else { + debug_assert!(false, "BoolKind::project saw non-boolean after precheck"); + unreachable!("BoolKind invariant violated") + } + } +} + +#[doc(hidden)] +pub struct StringKind; + +impl ValueElementKind for StringKind { + type Item<'a> = &'a str; + + fn precheck(slice: &[Value]) -> Result<(), Error> { + for v in slice { + if !matches!(v, Value::String(_)) { + return Err(Error::TypeError("expected string".into())); + } + } + Ok(()) + } + + fn project<'a>(v: &'a Value) -> Self::Item<'a> { + if let Value::String(s) = v { + s.as_str() + } else { + debug_assert!(false, "StringKind::project saw non-string after precheck"); + unreachable!("StringKind invariant violated") + } + } +} + +/// Borrowed iterator over a sequence of AST `Value` references. +pub type ValueIter<'a> = TypedValueIter<'a, ValueKind>; + +/// Borrowed iterator over numeric arguments, performing type checking +/// as elements are pulled. +pub type NumIter<'a> = TypedValueIter<'a, NumberKind>; + +/// Borrowed iterator over boolean arguments, performing type checking +/// as elements are pulled. +pub type BoolIter<'a> = TypedValueIter<'a, BoolKind>; + +/// Borrowed iterator over string arguments, performing type checking +/// as elements are pulled. +pub type StringIter<'a> = TypedValueIter<'a, StringKind>; + +// ===================================================================== +// Rest-parameter support for variadic operations +// ===================================================================== + +/// Core trait used to construct rest-parameter values from a slice of +/// AST arguments. +/// +/// The associated `Param<'a>` is the type actually seen by the builtin +/// function for a given lifetime of the underlying value slice. +pub(crate) trait FromRest { + type Param<'a>; + + fn from_rest<'a>(slice: &'a [Value]) -> Result, Error>; +} + +impl FromRest for TypedValueIter<'static, K> +where + K: ValueElementKind, +{ + type Param<'a> = TypedValueIter<'a, K>; + + fn from_rest<'a>(slice: &'a [Value]) -> Result, Error> { + TypedValueIter::::new(slice) + } +} + +// ===================================================================== +// Return-type adaptation for builtin functions +// ===================================================================== + +/// Internal trait that normalizes builtin return types to the +/// canonical `Result` expected by the evaluator. +/// +/// Builtins typically return either `Result` directly +/// or `Result` for some `T` that implements `Into`. +pub(crate) trait IntoValueResult { + fn into_value_result(self) -> Result; +} + +impl IntoValueResult for Result +where + T: Into, +{ + fn into_value_result(self) -> Result { + self.map(Into::into) + } +} + +impl IntoValueResult for T +where + T: Into, +{ + fn into_value_result(self) -> Result { + Ok(self.into()) + } +} + +/// Public trait for converting strongly-typed Rust functions or +/// closures into the erased [`OperationFn`], parameterized by an +/// argument tuple type. +/// +/// Builtins implemented via this trait may return any type `R` that +/// implements [`IntoValueResult`], typically `Result` +/// or `Result` where `T: Into`. +pub(crate) trait IntoOperation { + fn into_operation(self) -> Arc; +} + +/// Trait for operations registered via the variadic API. +/// +/// Implemented for functions whose Rust signature includes a variadic +/// "rest" parameter, expressed using the iterator types defined in +/// this module (`ValueIter<'a>`, `NumIter<'a>`, +/// `BoolIter<'a>`, or `StringIter<'a>`), optionally after a +/// fixed prefix of `FromParam` parameters. +pub(crate) trait IntoVariadicOperation { + fn into_variadic_operation(self) -> Arc; +} + +// ===================================================================== +// Variadic adapters using iterator-based rest parameters +// ===================================================================== + +/// Adapter for functions whose Rust signature consists only of a rest +/// parameter expressed via one of the iterator types in this module +/// (e.g. `ValueIter<'a>`, `NumIter<'a>`, etc.). +impl IntoVariadicOperation<(I,)> for F +where + I: FromRest, + F: for<'a> Fn(::Param<'a>) -> R + Send + Sync + 'static, + R: IntoValueResult, +{ + fn into_variadic_operation(self) -> Arc { + Arc::new(move |args: Vec| { + let rest_param: ::Param<'_> = ::from_rest(&args[..])?; + let result: R = (self)(rest_param); + result.into_value_result() + }) + } +} + +/// Helper macro to implement `IntoVariadicOperation` for functions +/// with a fixed prefix of `FromParam` parameters followed by a single +/// rest parameter expressed using one of the iterator types in this +/// module. +macro_rules! impl_into_variadic_operation_for_prefix_and_rest { + ($prefix:expr, $( $v:ident, $p:ident : $A:ident ),+ ) => { + impl IntoVariadicOperation<( $( $A, )+ I, )> for F + where + I: FromRest, + $( $A: FromParam, )+ + F: for<'a> Fn( + $( <$A as FromParam>::Param<'a> ),+, + ::Param<'a>, + ) -> R + + Send + + Sync + + 'static, + R: IntoValueResult, + { + fn into_variadic_operation(self) -> Arc { + Arc::new(move |mut args: Vec| { + let len = args.len(); + match args.as_mut_slice() { + &mut [ $( ref mut $v ),+, ref mut rest @ .. ] => { + $( + let $p: <$A as FromParam>::Param<'_> = + <$A as FromParam>::from_arg($v)?; + )+ + + let rest_param: ::Param<'_> = + ::from_rest(&*rest)?; + + let result: R = (self)( $( $p ),+, rest_param ); + result.into_value_result() + } + _ => Err(Error::arity_error($prefix, len)), + } + }) + } + } + }; +} + +impl_into_variadic_operation_for_prefix_and_rest!(1, v0, p0: A1); +impl_into_variadic_operation_for_prefix_and_rest!(2, v0, p0: A1, v1, p1: A2); +impl_into_variadic_operation_for_prefix_and_rest!(3, v0, p0: A1, v1, p1: A2, v2, p2: A3); +impl_into_variadic_operation_for_prefix_and_rest!(4, v0, p0: A1, v1, p1: A2, v2, p2: A3, v3, p3: A4); +impl_into_variadic_operation_for_prefix_and_rest!(5, v0, p0: A1, v1, p1: A2, v2, p2: A3, v3, p3: A4, v4, p4: A5); +impl_into_variadic_operation_for_prefix_and_rest!(6, v0, p0: A1, v1, p1: A2, v2, p2: A3, v3, p3: A4, v4, p4: A5, v5, p5: A6); +impl_into_variadic_operation_for_prefix_and_rest!(7, v0, p0: A1, v1, p1: A2, v2, p2: A3, v3, p3: A4, v4, p4: A5, v5, p5: A6, v6, p6: A7); + +// ===================================================================== +// Fixed-arity adapters +// ===================================================================== + +/// Helper macro to implement `IntoOperation` for functions of various +/// arities. +/// +/// It performs arity checking up front, then destructures the owned +/// `Vec` into local `Value` slots so that `FromParam` can either +/// borrow from or consume each argument as needed before invoking the +/// builtin function. +macro_rules! impl_into_operation_for_arity { + ($arity:expr, $( $v:ident, $p:ident : $A:ident ),+ ) => { + impl IntoOperation<( $( $A, )+ )> for F + where + F: for<'a> Fn( $( <$A as FromParam>::Param<'a> ),+ ) -> R + + Send + + Sync + + 'static, + $( $A: FromParam, )+ + R: IntoValueResult, + { + fn into_operation(self) -> Arc { + Arc::new(move |mut args: Vec| { + let len = args.len(); + match args.as_mut_slice() { + &mut [ $( ref mut $v ),+ ] => { + $( + let $p: <$A as FromParam>::Param<'_> = + <$A as FromParam>::from_arg($v)?; + )+ + + let result: R = (self)( $( $p ),+ ); + result.into_value_result() + } + _ => Err(Error::arity_error($arity, len)), + } + }) + } + } + }; +} + +// 0-arg functions / closures +impl IntoOperation<()> for F +where + F: Fn() -> R + Send + Sync + 'static, + R: IntoValueResult, +{ + fn into_operation(self) -> Arc { + Arc::new(move |args: Vec| { + if !args.is_empty() { + return Err(Error::arity_error(0, args.len())); + } + + let result: R = (self)(); + result.into_value_result() + }) + } +} + +impl_into_operation_for_arity!(1, v0, p0: A1); +impl_into_operation_for_arity!(2, v0, p0: A1, v1, p1: A2); +impl_into_operation_for_arity!(3, v0, p0: A1, v1, p1: A2, v2, p2: A3); +impl_into_operation_for_arity!(4, v0, p0: A1, v1, p1: A2, v2, p2: A3, v3, p3: A4); +impl_into_operation_for_arity!(5, v0, p0: A1, v1, p1: A2, v2, p2: A3, v3, p3: A4, v4, p4: A5); +impl_into_operation_for_arity!(6, v0, p0: A1, v1, p1: A2, v2, p2: A3, v3, p3: A4, v4, p4: A5, v5, p5: A6); +impl_into_operation_for_arity!(7, v0, p0: A1, v1, p1: A2, v2, p2: A3, v3, p3: A4, v4, p4: A5, v5, p5: A6, v6, p6: A7); +impl_into_operation_for_arity!(8, v0, p0: A1, v1, p1: A2, v2, p2: A3, v3, p3: A4, v4, p4: A5, v5, p5: A6, v6, p6: A7, v7, p7: A8);