Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, Error>
fn safe_div(a: i64, b: i64) -> Result<i64, Error> {
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<Value, Error>`
- `Result<T, Error>` where `T: Into<Value>` (for example `i64`,
`bool`, `&str`, arrays/vectors of those types, or `Vec<Value>`)
- bare `T` where `T: Into<Value>` (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
Expand Down
10 changes: 3 additions & 7 deletions examples/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -58,7 +58,7 @@ fn run_repl() {
// Handle special commands
match line {
":help" => {
_ = print_help(&[]).is_ok();
_ = print_help().is_ok();
continue;
}
":env" => {
Expand Down Expand Up @@ -144,11 +144,7 @@ fn run_repl() {
}
}

fn print_help(args: &[Value]) -> Result<Value, Error> {
if !args.is_empty() {
return Err(Error::arity_error(0, args.len()));
}

fn print_help() -> Result<Value, Error> {
println!("Mini Scheme Interpreter with JSONLogic Support:");
println!(" :help - Show this help message");
println!(" :env - Show current environment bindings");
Expand Down
91 changes: 80 additions & 11 deletions src/ast.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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),
Expand All @@ -77,7 +81,10 @@ pub enum Value {
/// Uses id string for equality comparison instead of function pointer
BuiltinFunction {
id: String,
func: fn(&[Value]) -> Result<Value, Error>,
// 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<OperationFn>,
},
/// User-defined functions (params, body, closure env)
Function {
Expand All @@ -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 {
Expand Down Expand Up @@ -146,6 +189,32 @@ impl<T: Into<Value> + Clone> From<&[T]> for Value {
}
}

// Fallible conversions from `Value` back into primitive Rust types.

impl std::convert::TryInto<NumberType> for Value {
type Error = Error;

fn try_into(self) -> Result<NumberType, Error> {
if let Value::Number(n) = self {
Ok(n)
} else {
Err(Error::TypeError("expected number".into()))
}
}
}

impl std::convert::TryInto<bool> for Value {
type Error = Error;

fn try_into(self) -> Result<bool, Error> {
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))]
Expand Down
Loading
Loading