Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## Introduction

Expression engine is a library written in pure Rust which provides an engine to compile and execute expressions. An expression indicates a string-like sentence that can be executed with some contexts and return a value (mostly, but not limited to, boolean, string and number).
Expression engine is a library written in pure Rust which parses expressions into AST, compiles them into bytecode, and executes them with context. An expression indicates a string-like sentence that can be executed with some contexts and return a value (mostly, but not limited to, boolean, string and number).

Expression engine aims to provide an engine for users that can execute complex logics using configurations without recompiling. It's a proper alternative as the basis to build business rule engines.

Expand Down
68 changes: 62 additions & 6 deletions benches/execute_expression.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use criterion::{criterion_group, criterion_main, Criterion};
use expression_engine::{create_context, execute, parse_expression, Value};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use expression_engine::{bytecode, create_context, execute, parse_expression, Value};
use std::sync::Arc;

fn bench_execute_expression(c: &mut Criterion) {
let input = "c = 5+3; c+=10+f; c";
Expand All @@ -8,9 +9,9 @@ fn bench_execute_expression(c: &mut Criterion) {
execute(
input,
create_context!(
"d" => 2,
"b" => true,
"f" => Arc::new(|_| Ok(Value::from(3)))
"d" => 2,
"b" => true,
"f" => Arc::new(|_| Ok(Value::from(3)))
),
)
})
Expand All @@ -22,5 +23,60 @@ fn bench_parse_expression(c: &mut Criterion) {
c.bench_function("parse_expression", |b| b.iter(|| parse_expression(input)));
}

criterion_group!(benches, bench_execute_expression, bench_parse_expression);
fn create_bench_context(with_func: bool) -> expression_engine::Context {
if with_func {
create_context!(
"d" => 2,
"b" => true,
"f" => Arc::new(|_| Ok(Value::from(3)))
)
} else {
create_context!(
"d" => 2,
"b" => true
)
}
}

fn bench_execution_only_ast_vs_bytecode(c: &mut Criterion) {
let scenarios = [
("short_expression", "1+2*3-4", false),
(
"long_expression",
"2+3*5-2/2+6*(2+4)-20+1+2+3+4+5+6+7+8+9+10",
false,
),
("function_call", "f(3)+2*f(4)", true),
("list_map_mix", "{'a':1+2, 'b':[1,2,3,4], 5: 6>2}", false),
("ternary_chain", "d > 1 ? (2 < 3 ? 11 : 12) : 13", false),
];

for (name, expr, with_func) in scenarios {
let ast = parse_expression(expr).unwrap();
let program = bytecode::compile_expression(&ast).unwrap();
let mut group = c.benchmark_group(format!("execution_only/{}", name));

group.bench_function("ast_exec", |b| {
b.iter(|| {
let mut ctx = create_bench_context(with_func);
black_box(ast.exec(&mut ctx).unwrap())
})
});

group.bench_function("bytecode_exec", |b| {
b.iter(|| {
let mut ctx = create_bench_context(with_func);
black_box(bytecode::execute_program(&program, &mut ctx).unwrap())
})
});
group.finish();
}
}

criterion_group!(
benches,
bench_execute_expression,
bench_parse_expression,
bench_execution_only_ast_vs_bytecode
);
criterion_main!(benches);
34 changes: 34 additions & 0 deletions benchmarks_execution_only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Execution-only benchmark: AST vs Bytecode VM
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot According to the current implementation, update the performance comparison data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in commit aa0d38b.

benchmarks_execution_only.md now reflects freshly re-run execution-only Criterion data from the current implementation (including the VM stack optimization), with updated per-scenario deltas and takeaways.

Screenshot: N/A (no UI changes; benchmark documentation update only).


Command:

```bash
cargo bench --bench execute_expression
```

Method:

- Parse each expression once to AST.
- Compile the AST once to bytecode `Program`.
- Benchmark **execution only**:
- `ast.exec(&mut ctx)`
- `bytecode::execute_program(&program, &mut ctx)`
- Create a fresh context per iteration for both paths.

## Results (Criterion, median)

| Scenario | AST exec | Bytecode exec | Delta (Bytecode vs AST) |
| --- | ---: | ---: | ---: |
| short_expression | 597.55 ns | 594.13 ns | -0.57% |
| long_expression | 2.5106 µs | 2.5698 µs | +2.36% |
| function_call | 695.27 ns | 655.10 ns | -5.78% |
| list_map_mix | 864.45 ns | 679.70 ns | -21.37% |
| ternary_chain | 493.75 ns | 494.15 ns | +0.08% |

## Takeaways

- At pure execution level, bytecode is **not uniformly slower**.
- Bytecode is close to parity for short-expression and ternary-chain cases in this run.
- Bytecode is slower in the long arithmetic-heavy case in this run.
- Bytecode is faster in function-call and list/map construction cases in this run.
- The previous end-to-end slowdown mostly comes from the extra **compile stage per call** in `execute()` (parse + compile + run).
Loading
Loading