rulesxp/
lib.rs

1//! RulesXP - Multi-language rules expression evaluator
2//!
3//! This crate provides a minimalistic expression evaluator that supports both Scheme syntax
4//! and JSONLogic operations with strict typing. It implements a proper subset of both
5//! languages designed for reliable rule evaluation with predictable behavior.
6//!
7//! ## Dual Language Support
8//!
9//! The evaluator accepts expressions in Scheme or JSONLogic syntax:
10//!
11//! ```scheme
12//! ;; Scheme syntax
13//! (+ 1 2 3)           ; arithmetic
14//! (if #t "yes" "no")  ; conditionals
15//! (and #t #f)         ; boolean logic
16//! (car '(1 2 3))      ; list operations
17//! ```
18//!
19//! The same operations can be represented in JSONLogic:
20//! ```json
21//! {"+": [1, 2, 3]}
22//! {"if": [true, "yes", "no"]}
23//! {"and": [true, false]}
24//! {"car": [[1, 2, 3]]}
25//! ```
26//!
27//! ## Strict Typing
28//!
29//! This interpreter implements stricter semantics than standard Scheme or JSONLogic:
30//! - No type coercion (numbers don't become strings, etc.)
31//! - Boolean operations require actual boolean values (no "truthiness")
32//! - Arithmetic overflow detection and error reporting
33//! - Strict arity checking for all functions
34//!
35//! Any program accepted by this interpreter will give identical results in standard
36//! Scheme R7RS-small or JSONLogic interpreters, but the converse is not true due to
37//! our additional type safety requirements.
38//!
39//! ## Modules
40//!
41//! - `scheme`: S-expression parsing from text
42//! - `evaluator`: Core expression evaluation engine
43//! - `jsonlogic`: JSONLogic format conversion and integration
44
45use std::fmt;
46
47/// Maximum parsing depth to prevent stack overflow attacks
48/// This limits deeply nested structures in both S-expression and JSONLogic parsers
49pub const MAX_PARSE_DEPTH: usize = 32;
50
51/// Maximum evaluation depth to prevent stack overflow in recursive evaluation
52/// This limits deeply nested function calls and expressions during evaluation
53/// Set higher than parse depth to allow for nested function applications
54pub const MAX_EVAL_DEPTH: usize = 64;
55
56/// Error types for the interpreter
57#[derive(Debug, Clone, PartialEq)]
58pub enum Error {
59    ParseError(String),
60    EvalError(String),
61    TypeError(String),
62    UnboundVariable(String),
63    ArityError {
64        expected: usize,
65        got: usize,
66        expression: Option<String>, // Optional expression context
67    },
68}
69
70impl Error {
71    /// Create an ArityError without expression context
72    pub fn arity_error(expected: usize, got: usize) -> Self {
73        Error::ArityError {
74            expected,
75            got,
76            expression: None,
77        }
78    }
79
80    /// Create an ArityError with expression context
81    pub fn arity_error_with_expr(expected: usize, got: usize, expression: String) -> Self {
82        Error::ArityError {
83            expected,
84            got,
85            expression: Some(expression),
86        }
87    }
88}
89
90impl fmt::Display for Error {
91    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
92        match self {
93            Error::ParseError(msg) => write!(f, "Parse error: {msg}"),
94            Error::EvalError(msg) => write!(f, "Evaluation error: {msg}"),
95            Error::TypeError(msg) => write!(f, "Type error: {msg}"),
96            Error::UnboundVariable(var) => write!(f, "Unbound variable: {var}"),
97            Error::ArityError {
98                expected,
99                got,
100                expression,
101            } => match expression {
102                Some(expr) => write!(
103                    f,
104                    "Arity error in expression {expr}: expected {expected} arguments, got {got}"
105                ),
106                None => write!(f, "Arity error: expected {expected} arguments, got {got}"),
107            },
108        }
109    }
110}
111
112mod ast;
113mod builtinops;
114pub mod evaluator;
115
116// Re-export the core Value type at the crate root so that
117// callers do not need to depend on the internal `ast` module.
118pub use ast::Value;
119
120#[cfg(feature = "jsonlogic")]
121pub mod jsonlogic;
122
123#[cfg(feature = "scheme")]
124pub mod scheme;