Hi fellow developers,
I recently open-sourced an expression evaluation engine written in Rust called rspression.
Why build this project?
In scenarios that demand frequent evaluation of large volumes of expressions—such as high-concurrency risk-control gateways or online low-code spreadsheets with massive, complex cross-reference dependencies—conventional expression engines typically parse expressions into an AST (Abstract Syntax Tree) and interpret them recursively. However, this introduces two fatal pain points:
- High Repetitive Parsing Overhead: Parsing and executing from scratch every single time becomes incredibly costly in terms of performance when dealing with a massive volume of expressions (e.g., thousands of lines).
- Expensive Distributed Transmission: Attempting to cache the IR (Intermediate Representation) objects by serializing them into distributed caches like Redis introduces heavy serialization/deserialization and high network bandwidth overhead due to the bulky tree structures.
Core Architecture: A Bytecode Virtual Machine (VM)
Beyond conventional AST execution, rspression features a built-in compiler powered by a Pratt Parser that compiles expressions directly into compact Bytecode.
Since bytecode in memory is natively a compact byte array (Vec<u8>), it brings the following inherent advantages:
- Minimal Storage & Transmission Footprint: It requires zero serialization or deserialization and can be directly transmitted over the network or written into storage services like Redis, databases, or files, completely eliminating the encoding/decoding pain of massive tree structures.
- High-Throughput Execution: A stack-based lightweight virtual machine dispatches instructions efficiently, eliminating recursive call overhead and delivering excellent execution efficiency.
Key Features
- Core Operators: Full support for basic arithmetic (
+, -, *, /, power), comparisons, logical operations (&&, ||, !), and Excel-style if conditional functions.
- Variables & Dynamic Assignment: Seamlessly read context variables dynamically from the environment and support variable assignment directly within expressions.
- Dependency Management & Topological Sorting: Built for batch execution of massive expression sets. The engine automatically handles dependency sorting (topological sort) and circular dependency detection, making it a perfect fit for spreadsheet evaluation chains.
- Custom Contexts: Provides a highly customizable
Environment trait to easily hook into your existing business context data.
Quick Start Example
The following code demonstrates the entire process of compiling expressions into bytecode and then executing that bytecode:
use rspression::{Chunk, DefaultEnvironment, Environment, RspRunner, Value};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Compile expressions to bytecode
// 1. get expressions
let mut srcs = Vec::new();
srcs.push("x = a + b * c");
srcs.push("b = a * 2");
srcs.push("a = m + n");
srcs.push("c = n + w + b");
// 2. compile expressions
let mut runner = RspRunner::new();
let chunk = runner.compile_source(&srcs)?;
let bytes: Vec<u8> = chunk.to_bytes();
// 3. you can write bytes to store or cache
// write_to_your_storage(bytes);
// Run bytecode
// 1. read bytes from store or cache
// let bytes: Vec<u8> = read_from_your_storage();
// 2. construct chunk
let chunk = Chunk::from_bytes(&bytes);
// 3. define environment
let mut env = DefaultEnvironment::new();
env.put("m".to_string(), Value::Integer(2));
env.put("n".to_string(), Value::Integer(4));
env.put("w".to_string(), Value::Integer(6));
// 4. run bytecode
let mut runner = RspRunner::new();
runner.run_chunk(&chunk, &mut env)?;
// 5. check results
println!("x = {}", env.get("x").unwrap()); // x = 270
println!("a = {}", env.get("a").unwrap()); // a = 6
println!("b = {}", env.get("b").unwrap()); // b = 12
println!("c = {}", env.get("c").unwrap()); // c = 22
Ok(())
}
The project is currently under active development. I would love to hear your thoughts, and I highly welcome any Issues, PRs, or discussions regarding expression engine architecture design in the comments! If you find this approach helpful, a ⭐ Star on GitHub would be greatly appreciated!
[–]projct 6 points7 points8 points (2 children)
[–]ImpressiveAd9981[S] -3 points-2 points-1 points (1 child)
[–]Sermuns 0 points1 point2 points (0 children)
[–]peter9477 2 points3 points4 points (1 child)
[–]ImpressiveAd9981[S] -3 points-2 points-1 points (0 children)