From 30a80ea13af33ca7f99e89e3162730cd05d874f2 Mon Sep 17 00:00:00 2001 From: Eugene Talagrand Date: Sat, 22 Nov 2025 09:26:55 -0800 Subject: [PATCH 1/5] refactor!: Simplify project hierarchy --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 7 +++---- examples/repl.rs | 4 ++-- src/builtinops.rs | 43 ++----------------------------------------- src/evaluator.rs | 45 ++++++++++++++++++++++++++++++++++++++++++--- src/jsonlogic.rs | 5 ++--- src/lib.rs | 9 ++++++--- src/scheme.rs | 2 +- 9 files changed, 60 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8553d67..3c4d35f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,7 +161,7 @@ dependencies = [ [[package]] name = "rulesxp" -version = "0.2.1" +version = "0.3.0" dependencies = [ "nom", "rustyline", diff --git a/Cargo.toml b/Cargo.toml index 4db6ff5..6a3649d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rulesxp" -version = "0.2.1" +version = "0.3.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 1e03b49..bdbff45 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Rust functions. These are registered as **builtin operations** on the #### Fixed-arity builtins ```rust -use rulesxp::{Error, ast::Value, evaluator}; +use rulesxp::{Error, Value, evaluator}; // Infallible builtin: returns a bare i64 fn add2(a: i64, b: i64) -> i64 { @@ -187,9 +187,8 @@ 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}; +use rulesxp::{Error, Value, evaluator}; +use rulesxp::evaluator::{Arity, NumIter, ValueIter}; // Single list argument: (sum-list (list 1 2 3 4)) => 10 fn sum_list(nums: NumIter<'_>) -> i64 { diff --git a/examples/repl.rs b/examples/repl.rs index abeeeeb..9276c79 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -1,5 +1,5 @@ use rulesxp::Error; -use rulesxp::ast::Value; +use rulesxp::Value; use rulesxp::evaluator; use rulesxp::jsonlogic::{ast_to_jsonlogic, parse_jsonlogic}; use rulesxp::scheme::parse_scheme; @@ -197,7 +197,7 @@ fn print_environment(env: &rulesxp::evaluator::Environment) { for (name, value) in bindings { match value { - rulesxp::ast::Value::BuiltinFunction { .. } => builtins.push(name), + Value::BuiltinFunction { .. } => builtins.push(name), _ => user_defined.push((name, value)), } } diff --git a/src/builtinops.rs b/src/builtinops.rs index 75f6054..4852d97 100644 --- a/src/builtinops.rs +++ b/src/builtinops.rs @@ -53,51 +53,12 @@ use crate::Error; use crate::ast::{NumberType, Value}; use crate::evaluator::intooperation::{IntoOperation, IntoVariadicOperation, OperationFn}; use crate::evaluator::{ - Environment, NumIter, StringIter, ValueIter, eval_and, eval_define, eval_if, eval_lambda, - eval_or, eval_quote, + Arity, Environment, NumIter, StringIter, ValueIter, eval_and, eval_define, eval_if, + eval_lambda, eval_or, eval_quote, }; use std::collections::HashMap; use std::sync::{Arc, LazyLock}; -/// Represents the expected number of arguments for an operation -#[derive(Debug, Clone, PartialEq)] -pub enum Arity { - /// Exactly n arguments required - Exact(usize), - /// At least n arguments required - AtLeast(usize), - /// Between min and max arguments (inclusive) - Range(usize, usize), - /// Any number of arguments (0 or more) - Any, -} - -impl Arity { - /// Check if the given number of arguments is valid for this arity constraint - pub(crate) fn validate(&self, arg_count: usize) -> Result<(), Error> { - let valid = match self { - Arity::Exact(n) => arg_count == *n, - Arity::AtLeast(n) => arg_count >= *n, - Arity::Range(min, max) => arg_count >= *min && arg_count <= *max, - Arity::Any => true, - }; - - if valid { - Ok(()) - } else { - Err(Error::ArityError { - expected: match self { - Arity::Exact(n) | Arity::AtLeast(n) => *n, - Arity::Range(min, _) => *min, - Arity::Any => 0, - }, - got: arg_count, - expression: None, // Builtin validation doesn't have expression context - }) - } - } -} - /// Represents the implementation of a built-in expression (function or special form) #[derive(Clone)] pub enum OpKind { diff --git a/src/evaluator.rs b/src/evaluator.rs index a13d531..1ea43f2 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -2,13 +2,52 @@ use self::intooperation::{IntoOperation, IntoVariadicOperation}; use crate::Error; use crate::MAX_EVAL_DEPTH; use crate::ast::Value; -use crate::builtinops::{Arity, get_builtin_ops}; +use crate::builtinops::get_builtin_ops; use std::sync::Arc; pub(crate) mod intooperation; pub use self::intooperation::{BoolIter, NumIter, StringIter, ValueIter}; use std::collections::HashMap; +/// Represents the expected number of arguments for an operation. +#[derive(Debug, Clone, PartialEq)] +pub enum Arity { + /// Exactly n arguments required + Exact(usize), + /// At least n arguments required + AtLeast(usize), + /// Between min and max arguments (inclusive) + Range(usize, usize), + /// Any number of arguments (0 or more) + Any, +} + +impl Arity { + /// Check if the given number of arguments is valid for this arity constraint. + pub(crate) fn validate(&self, arg_count: usize) -> Result<(), Error> { + let valid = match self { + Arity::Exact(n) => arg_count == *n, + Arity::AtLeast(n) => arg_count >= *n, + Arity::Range(min, max) => arg_count >= *min && arg_count <= *max, + Arity::Any => true, + }; + + if valid { + Ok(()) + } else { + Err(Error::ArityError { + expected: match self { + Arity::Exact(n) | Arity::AtLeast(n) => *n, + Arity::Range(min, _) => *min, + Arity::Any => 0, + }, + got: arg_count, + expression: None, // Builtin validation doesn't have expression context + }) + } + } +} + /// Environment for variable bindings #[derive(Debug, Clone, PartialEq, Default)] pub struct Environment { @@ -46,7 +85,7 @@ impl Environment { /// /// This allows writing natural Rust functions like: /// - /// ```rust,ignore + /// ```rust /// use rulesxp::{Error, evaluator}; /// /// // Infallible builtin: returns a bare i64 @@ -62,7 +101,7 @@ impl Environment { /// a Value or a type convertible into Value, and encode /// their own error messages: /// - /// ```rust,ignore + /// ```rust /// use rulesxp::{Error, evaluator}; /// /// fn safe_div(a: i64, b: i64) -> Result { diff --git a/src/jsonlogic.rs b/src/jsonlogic.rs index 1280fba..0beccae 100644 --- a/src/jsonlogic.rs +++ b/src/jsonlogic.rs @@ -1,9 +1,8 @@ use crate::Error; use crate::MAX_PARSE_DEPTH; use crate::ast::{Value, is_valid_symbol}; -use crate::builtinops::{ - Arity, BuiltinOp, find_jsonlogic_op, find_scheme_op, get_list_op, get_quote_op, -}; +use crate::builtinops::{BuiltinOp, find_jsonlogic_op, find_scheme_op, get_list_op, get_quote_op}; +use crate::evaluator::Arity; /// Indicates the compilation context for JSON values #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/lib.rs b/src/lib.rs index d800b06..4339a79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,6 @@ //! //! - `scheme`: S-expression parsing from text //! - `evaluator`: Core expression evaluation engine -//! - `builtinops`: Built-in operations with dual-language mapping //! - `jsonlogic`: JSONLogic format conversion and integration use std::fmt; @@ -110,10 +109,14 @@ impl fmt::Display for Error { } } -pub mod ast; -pub mod builtinops; +mod ast; +mod builtinops; pub mod evaluator; +// Re-export the core Value type at the crate root so that +// callers do not need to depend on the internal `ast` module. +pub use ast::Value; + #[cfg(feature = "jsonlogic")] pub mod jsonlogic; diff --git a/src/scheme.rs b/src/scheme.rs index f335755..5e27ed0 100644 --- a/src/scheme.rs +++ b/src/scheme.rs @@ -290,7 +290,7 @@ fn parse_quote( )) } -/// Parse a complete S-expression from input with optimization enabled +/// Parse a complete S-expression from input. pub fn parse_scheme(input: &str) -> Result { match terminated( |input| parse_sexpr(input, ShouldPrecompileOps::Yes, 0), From 38bccc3a7ce81416eab1c50f5c55f10888e3b190 Mon Sep 17 00:00:00 2001 From: Eugene Talagrand Date: Sun, 30 Nov 2025 20:25:43 -0800 Subject: [PATCH 2/5] perf: Introduce perf benches in CI --- .github/workflows/benchmark.yml | 45 ++++ Cargo.lock | 430 ++++++++++++++++++++++++++++++++ Cargo.toml | 6 + benches/bench_main.rs | 76 ++++++ 4 files changed, 557 insertions(+) create mode 100644 .github/workflows/benchmark.yml create mode 100644 benches/bench_main.rs diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..f1d2668 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,45 @@ +name: Benchmark + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: write + deployments: write + +jobs: + benchmark: + name: Run Criterion Benchmarks + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Run benchmark + # We use --output-format bencher to make the output compatible with the + # github-action-benchmark action's 'cargo' tool parser. + run: cargo bench --bench bench_main --all-features -- --output-format bencher | tee output.txt + + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: RulesXP Benchmark + tool: 'cargo' + output-file-path: output.txt + github-token: ${{ secrets.GITHUB_TOKEN }} + # Push and deploy GitHub pages branch automatically + auto-push: true + # Show alert with commit comment on detecting possible performance regression + alert-threshold: '125%' + comment-on-alert: true + fail-on-alert: true + alert-comment-cc-users: '@${{ github.actor }}' diff --git a/Cargo.lock b/Cargo.lock index 3c4d35f..f102ac7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,51 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cfg-if" version = "1.0.4" @@ -20,6 +59,58 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + [[package]] name = "clipboard-win" version = "5.4.1" @@ -29,6 +120,79 @@ dependencies = [ "error-code", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "endian-type" version = "0.1.2" @@ -62,6 +226,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "home" version = "0.5.12" @@ -71,12 +252,42 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.177" @@ -131,6 +342,55 @@ dependencies = [ "memchr", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -159,10 +419,60 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "rulesxp" version = "0.3.0" dependencies = [ + "criterion", "nom", "rustyline", "serde_json", @@ -181,6 +491,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "rustyline" version = "17.0.2" @@ -209,6 +525,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.228" @@ -216,6 +541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -268,6 +594,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -292,6 +628,80 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -453,3 +863,23 @@ name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 6a3649d..f52a400 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,12 @@ serde_json = { version = "1.0", optional = true } [dev-dependencies] rustyline = "17.0" +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "bench_main" +harness = false +required-features = ["scheme", "jsonlogic"] [lints.clippy] unwrap_used = "warn" diff --git a/benches/bench_main.rs b/benches/bench_main.rs new file mode 100644 index 0000000..9c97177 --- /dev/null +++ b/benches/bench_main.rs @@ -0,0 +1,76 @@ +#![allow(clippy::unwrap_used)] + +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use rulesxp::{evaluator, jsonlogic, scheme}; + +const SCHEME_SIMPLE: &str = "(+ 1 2)"; +const SCHEME_NESTED: &str = "(if (> (* 5 2) 8) (max 10 5 20) 0)"; +const JSONLOGIC_SIMPLE: &str = r#"{"+":[1,2]}"#; +const JSONLOGIC_NESTED: &str = r#"{"if":[{">":[{"*":[5,2]},8]},{"max":[10,5,20]},0]}"#; + +// Recursive Factorial (using inline self-application pattern) +const SCHEME_FACTORIAL: &str = + "((lambda (f x) (f f x)) (lambda (self n) (if (<= n 1) 1 (* n (self self (- n 1))))) 10)"; + +fn bench_parsing(c: &mut Criterion) { + let mut group = c.benchmark_group("Parsing"); + + // Scheme Parsing + group.bench_function("Scheme Simple", |b| { + b.iter(|| scheme::parse_scheme(black_box(SCHEME_SIMPLE))) + }); + + group.bench_function("Scheme Nested", |b| { + b.iter(|| scheme::parse_scheme(black_box(SCHEME_NESTED))) + }); + + group.bench_function("Scheme Factorial", |b| { + b.iter(|| scheme::parse_scheme(black_box(SCHEME_FACTORIAL))) + }); + + // JSONLogic Parsing + group.bench_function("JSONLogic Simple", |b| { + b.iter(|| jsonlogic::parse_jsonlogic(black_box(JSONLOGIC_SIMPLE))) + }); + + group.bench_function("JSONLogic Nested", |b| { + b.iter(|| jsonlogic::parse_jsonlogic(black_box(JSONLOGIC_NESTED))) + }); + + group.finish(); +} + +fn bench_evaluation(c: &mut Criterion) { + let mut group = c.benchmark_group("Evaluation"); + + let env = evaluator::create_global_env(); + let scheme_expr = scheme::parse_scheme(SCHEME_SIMPLE).unwrap(); + let json_expr = jsonlogic::parse_jsonlogic(JSONLOGIC_SIMPLE).unwrap(); + let scheme_nested = scheme::parse_scheme(SCHEME_NESTED).unwrap(); + let json_nested = jsonlogic::parse_jsonlogic(JSONLOGIC_NESTED).unwrap(); + let scheme_factorial = scheme::parse_scheme(SCHEME_FACTORIAL).unwrap(); + group.bench_function("Eval Scheme Simple", |b| { + b.iter(|| evaluator::eval(black_box(&scheme_expr), &mut env.clone())) + }); + + group.bench_function("Eval JSONLogic Simple", |b| { + b.iter(|| evaluator::eval(black_box(&json_expr), &mut env.clone())) + }); + + group.bench_function("Eval Scheme Nested", |b| { + b.iter(|| evaluator::eval(black_box(&scheme_nested), &mut env.clone())) + }); + + group.bench_function("Eval JSONLogic Nested", |b| { + b.iter(|| evaluator::eval(black_box(&json_nested), &mut env.clone())) + }); + + group.bench_function("Eval Scheme Factorial", |b| { + b.iter(|| evaluator::eval(black_box(&scheme_factorial), &mut env.clone())) + }); + + group.finish(); +} + +criterion_group!(benches, bench_parsing, bench_evaluation); +criterion_main!(benches); From 53bb445ff924dcc754f119d0c26533ed1bcee21e Mon Sep 17 00:00:00 2001 From: Eugene Talagrand Date: Sun, 30 Nov 2025 20:26:43 -0800 Subject: [PATCH 3/5] build(dep): update dependencies --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f102ac7..2949563 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -585,9 +585,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", From 4ee51ba93302b5872423f6d8c180a3c6b6d1d8cb Mon Sep 17 00:00:00 2001 From: Eugene Talagrand Date: Sun, 30 Nov 2025 21:24:40 -0800 Subject: [PATCH 4/5] style: clippy clean when not all features are enabled --- src/ast.rs | 2 ++ src/builtinops.rs | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ast.rs b/src/ast.rs index 037843a..303c15d 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -19,11 +19,13 @@ pub(crate) type NumberType = i64; /// Allowed non-alphanumeric characters in Scheme symbol names /// Most represent mathematical symbols or predicates ("?"), "$" supported for JavaScript identifiers +#[cfg(any(feature = "scheme", feature = "jsonlogic"))] pub(crate) const SYMBOL_SPECIAL_CHARS: &str = "+-*/<>=!?_$"; /// Check if a string is a valid symbol name /// Valid: non-empty, no leading digit, no "-digit" prefix, alphanumeric + SYMBOL_SPECIAL_CHARS /// Note: This function is tested as part of the parser tests in parser.rs +#[cfg(any(feature = "scheme", feature = "jsonlogic"))] pub(crate) fn is_valid_symbol(name: &str) -> bool { let mut chars = name.chars(); diff --git a/src/builtinops.rs b/src/builtinops.rs index 4852d97..e4eb19a 100644 --- a/src/builtinops.rs +++ b/src/builtinops.rs @@ -56,6 +56,7 @@ use crate::evaluator::{ Arity, Environment, NumIter, StringIter, ValueIter, eval_and, eval_define, eval_if, eval_lambda, eval_or, eval_quote, }; +#[cfg(any(feature = "scheme", feature = "jsonlogic", test))] use std::collections::HashMap; use std::sync::{Arc, LazyLock}; @@ -112,12 +113,13 @@ impl PartialEq for BuiltinOp { impl BuiltinOp { /// Check if this operation is a special form - #[cfg_attr(not(test), expect(dead_code))] + #[cfg(test)] pub(crate) fn is_special_form(&self) -> bool { matches!(self.op_kind, OpKind::SpecialForm(_)) } /// Check if the given number of arguments is valid for this operation + #[cfg(any(feature = "scheme", feature = "jsonlogic"))] pub(crate) fn validate_arity(&self, arg_count: usize) -> Result<(), Error> { self.arity.validate(arg_count) } @@ -518,12 +520,14 @@ static BUILTIN_OPS: LazyLock> = LazyLock::new(|| { }); /// Lazy static map from scheme_id to BuiltinOp (private - use find_builtin_op_by_scheme_id) +#[cfg(any(feature = "scheme", feature = "jsonlogic", test))] 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) +#[cfg(any(feature = "jsonlogic", test))] static BUILTIN_JSONLOGIC: LazyLock> = LazyLock::new(|| { let ops: &'static [BuiltinOp] = BUILTIN_OPS.as_slice(); @@ -536,21 +540,25 @@ pub(crate) fn get_builtin_ops() -> &'static [BuiltinOp] { } /// Find a builtin operation by its Scheme identifier +#[cfg(any(feature = "scheme", feature = "jsonlogic", test))] pub(crate) fn find_scheme_op(id: &str) -> Option<&'static BuiltinOp> { BUILTIN_SCHEME.get(id).copied() } /// Find a builtin operation by its JSONLogic identifier +#[cfg(any(feature = "jsonlogic", test))] pub(crate) fn find_jsonlogic_op(id: &str) -> Option<&'static BuiltinOp> { BUILTIN_JSONLOGIC.get(id).copied() } /// Get the quote builtin operation - guaranteed to exist +#[cfg(any(feature = "scheme", feature = "jsonlogic"))] pub(crate) fn get_quote_op() -> &'static BuiltinOp { find_scheme_op("quote").expect("quote builtin operation must be available") } /// Get the list builtin operation - guaranteed to exist +#[cfg(feature = "jsonlogic")] pub(crate) fn get_list_op() -> &'static BuiltinOp { find_scheme_op("list").expect("list builtin operation must be available") } From b44795bc593001a9c481a13f4ed6f5fea1deea1c Mon Sep 17 00:00:00 2001 From: Eugene Talagrand Date: Sun, 30 Nov 2025 22:34:27 -0800 Subject: [PATCH 5/5] ci: Restrict benchmark uploads and comments to main and PRs with proper permissions --- .github/workflows/benchmark.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index f1d2668..e8715f6 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -37,9 +37,9 @@ jobs: output-file-path: output.txt github-token: ${{ secrets.GITHUB_TOKEN }} # Push and deploy GitHub pages branch automatically - auto-push: true + auto-push: ${{ github.ref == 'refs/heads/main' }} # Show alert with commit comment on detecting possible performance regression alert-threshold: '125%' - comment-on-alert: true + comment-on-alert: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} fail-on-alert: true alert-comment-cc-users: '@${{ github.actor }}'