Skip to content

Commit eb0041b

Browse files
authored
Migrate runtime from AST post-order execution to bytecode IR + VM with execution-only benchmarks and VM stack optimization (#64)
1 parent fddc8ae commit eb0041b

5 files changed

Lines changed: 559 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## Introduction
88

9-
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).
9+
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).
1010

1111
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.
1212

benches/execute_expression.rs

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use criterion::{criterion_group, criterion_main, Criterion};
2-
use expression_engine::{create_context, execute, parse_expression, Value};
1+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
2+
use expression_engine::{bytecode, create_context, execute, parse_expression, Value};
3+
use std::sync::Arc;
34

45
fn bench_execute_expression(c: &mut Criterion) {
56
let input = "c = 5+3; c+=10+f; c";
@@ -8,9 +9,9 @@ fn bench_execute_expression(c: &mut Criterion) {
89
execute(
910
input,
1011
create_context!(
11-
"d" => 2,
12-
"b" => true,
13-
"f" => Arc::new(|_| Ok(Value::from(3)))
12+
"d" => 2,
13+
"b" => true,
14+
"f" => Arc::new(|_| Ok(Value::from(3)))
1415
),
1516
)
1617
})
@@ -22,5 +23,60 @@ fn bench_parse_expression(c: &mut Criterion) {
2223
c.bench_function("parse_expression", |b| b.iter(|| parse_expression(input)));
2324
}
2425

25-
criterion_group!(benches, bench_execute_expression, bench_parse_expression);
26+
fn create_bench_context(with_func: bool) -> expression_engine::Context {
27+
if with_func {
28+
create_context!(
29+
"d" => 2,
30+
"b" => true,
31+
"f" => Arc::new(|_| Ok(Value::from(3)))
32+
)
33+
} else {
34+
create_context!(
35+
"d" => 2,
36+
"b" => true
37+
)
38+
}
39+
}
40+
41+
fn bench_execution_only_ast_vs_bytecode(c: &mut Criterion) {
42+
let scenarios = [
43+
("short_expression", "1+2*3-4", false),
44+
(
45+
"long_expression",
46+
"2+3*5-2/2+6*(2+4)-20+1+2+3+4+5+6+7+8+9+10",
47+
false,
48+
),
49+
("function_call", "f(3)+2*f(4)", true),
50+
("list_map_mix", "{'a':1+2, 'b':[1,2,3,4], 5: 6>2}", false),
51+
("ternary_chain", "d > 1 ? (2 < 3 ? 11 : 12) : 13", false),
52+
];
53+
54+
for (name, expr, with_func) in scenarios {
55+
let ast = parse_expression(expr).unwrap();
56+
let program = bytecode::compile_expression(&ast).unwrap();
57+
let mut group = c.benchmark_group(format!("execution_only/{}", name));
58+
59+
group.bench_function("ast_exec", |b| {
60+
b.iter(|| {
61+
let mut ctx = create_bench_context(with_func);
62+
black_box(ast.exec(&mut ctx).unwrap())
63+
})
64+
});
65+
66+
group.bench_function("bytecode_exec", |b| {
67+
b.iter(|| {
68+
let mut ctx = create_bench_context(with_func);
69+
black_box(bytecode::execute_program(&program, &mut ctx).unwrap())
70+
})
71+
});
72+
group.finish();
73+
}
74+
}
75+
76+
criterion_group!(
77+
benches,
78+
bench_execute_expression,
79+
bench_parse_expression,
80+
bench_execution_only_ast_vs_bytecode
81+
);
2682
criterion_main!(benches);

benchmarks_execution_only.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Execution-only benchmark: AST vs Bytecode VM
2+
3+
Command:
4+
5+
```bash
6+
cargo bench --bench execute_expression
7+
```
8+
9+
Method:
10+
11+
- Parse each expression once to AST.
12+
- Compile the AST once to bytecode `Program`.
13+
- Benchmark **execution only**:
14+
- `ast.exec(&mut ctx)`
15+
- `bytecode::execute_program(&program, &mut ctx)`
16+
- Create a fresh context per iteration for both paths.
17+
18+
## Results (Criterion, median)
19+
20+
| Scenario | AST exec | Bytecode exec | Delta (Bytecode vs AST) |
21+
| --- | ---: | ---: | ---: |
22+
| short_expression | 597.55 ns | 594.13 ns | -0.57% |
23+
| long_expression | 2.5106 µs | 2.5698 µs | +2.36% |
24+
| function_call | 695.27 ns | 655.10 ns | -5.78% |
25+
| list_map_mix | 864.45 ns | 679.70 ns | -21.37% |
26+
| ternary_chain | 493.75 ns | 494.15 ns | +0.08% |
27+
28+
## Takeaways
29+
30+
- At pure execution level, bytecode is **not uniformly slower**.
31+
- Bytecode is close to parity for short-expression and ternary-chain cases in this run.
32+
- Bytecode is slower in the long arithmetic-heavy case in this run.
33+
- Bytecode is faster in function-call and list/map construction cases in this run.
34+
- The previous end-to-end slowdown mostly comes from the extra **compile stage per call** in `execute()` (parse + compile + run).

0 commit comments

Comments
 (0)