mbel — a MoonBit expression language: Jexl-compatible dynamic evaluation with an expr-lang syntax front-end, dual tree-walk/bytecode-VM engines, predicate aggregates and resource budgets.
$ moon run cmd/main -- "1 + 2 * 3"
7
$ moon run cmd/main -- "'hello' + ' ' + 'world'"
"hello world"
$ moon run cmd/main -- "6+x*2>10 ? 'big' : 'small'" '{"x": 3}'
"big"
$ moon run cmd/main -- "age * (3 - 1)" '{"age": 36}'
72
$ moon run cmd/main -- 'assoc[.first == "Lana"].last' \
'{"assoc": [{"first": "Lana", "last": "Kane"}, {"first": "Cyril", "last": "Figgis"}]}'
"Kane"
$ moon run cmd/main -- "user.name + ' scored ' + (score * 10)" \
'{"user": {"name": "alice"}, "score": 8.5}'
"alice scored 85"
$ moon run cmd/main -- "items[.price <= 2].name" \
'{"items": [{"name": "apple", "price": 1.5}, {"name": "pear", "price": 3}]}'
"apple"$ moon run cmd/main -- "map(nums, # * 2)" '{"nums": [1, 2, 3, 4, 5]}'
[2,4,6,8,10]
$ moon run cmd/main -- "filter(users, .age >= 18)[0].name" \
'{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 15}]}'
"Alice"
$ moon run cmd/main -- "sum(1..100)"
5050
$ moon run cmd/main -- "count(users, .age >= 18)" \
'{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 15}, {"name": "Cyx", "age": 24}]}'
2
$ moon run cmd/main -- "groupBy(nums, # % 2)" '{"nums": [1, 2, 3, 4, 5]}'
{"1":[1,3,5],"0":[2,4]}
$ moon run cmd/main -- "reduce(nums, #acc + #, 100)" '{"nums": [1, 2, 3, 4, 5]}'
115$ moon run cmd/main -- "[1,2,3]|first"
1
$ moon run cmd/main -- "5|dbl|dbl"
20import {
"dimon-83/mbel/ast",
"dimon-83/mbel/evaluator",
"dimon-83/mbel/engine",
}
let inst = @engine.new()
// Evaluate against a context
let ctx = @ast.ObjectVal([
("user", @ast.ObjectVal([("age", @ast.NumVal(36.0))])),
])
let v = try {
@engine.Engine::eval(inst, "user.age > 18 ? 'adult' : 'minor'", ctx)
} catch {
@engine.EngineErr(msg) => abort(msg)
} // StrVal("adult")
// Register transforms / functions / operators
@engine.Engine::add_transform(
inst,
"dbl",
fn(args : Array[@ast.Value]) -> @ast.Value {
@ast.NumVal(@evaluator.to_number(args[0]) * 2.0)
},
)
let doubled = try {
@engine.Engine::eval(inst, "21|dbl", @ast.ObjectVal([]))
} catch {
@engine.EngineErr(msg) => abort(msg)
} // NumVal(42.0)
// expr-style resource budgets: nodes at parse time, depth + steps at
// eval time (0 disables a limit)
@engine.Engine::set_limits(inst, 10000, 10000, 1000000)// Walk — the reference tree-walk interpreter (default)
let walk_inst = @engine.new()
@engine.Engine::set_engine(walk_inst, Walk)
// Vm — compiles the (constant-folded) AST to bytecode once per
// Expression, caches the program, and runs a 16-opcode stack machine
let vm_inst = @engine.new()
@engine.Engine::set_engine(vm_inst, Vm)
// Both engines run the same expressions, transforms, and budgets
let ctx = @ast.ObjectVal([("x", @ast.NumVal(3.0))])
let a = @engine.Engine::eval(walk_inst, "6+x*2>10 ? 'big' : 'small'", ctx)
let b = @engine.Engine::eval(vm_inst, "6+x*2>10 ? 'big' : 'small'", ctx)
// value_equal(a, b) — always true; vm_parity tests enforce it
// The CLI can run either engine too:
// moon run cmd/main -- "2+2*3" "" vm$ moon run cmd/main -- "median([1, 9, 5])"
5
$ moon run cmd/main -- "fromBase64(toBase64("héllo ✓"))"
"héllo ✓"
$ moon run cmd/main -- "missing ?? 'fallback'"
"fallback"
$ moon run cmd/main -- "sort([3,1,2]) | first"
1⚠️ The Vm columns of earlier reports (2026-09-06, incl. the tables in docs/perf-report.md) are invalid: the end-to-end Expression::eval never dispatched to the Vm engine until f37f10d, so those "Vm" measurements were the tree-walk engine. The tables below and docs/coverage-vs-expr.md §7 are the first genuine dual-engine data.
| Scenario | Walk | Vm |
|---|---|---|
| Precompiled eval (constant-folded) | 13.2 ns (22.9) | 23.2 ns (50.0) |
| Ternary + logic + ?? chain | 107 ns (112) | 76.1 ns (85.8) |
| 50-term un-foldable chain | 1.60 µs (1.35) | 846 ns (615) |
| End-to-end compile + eval | 6.33 µs (13.8) | 6.37 µs (14.4) |
| 100-item relative filter | 4.48 µs (5.50) | 3.72 µs (3.10) |
| 100-item long-predicate filter | 16.3 µs (24.6) | 10.0 µs (7.60) |
| Aggregate map over 100 items | 3.57 µs (4.31) | 3.33 µs (3.03) |
| Aggregate filter+sum over 100 items | 6.09 µs (7.02) | 4.76 µs (4.17) |
| String builtin chain | 527 ns (520) | 472 ns (540) |
| toJSON/fromJSON roundtrip | 639 ns (839) | 619 ns (932) |
| Tokenize only (engine-independent) | 3.10 µs (5.88) | — |
moon test # 168 unit/parity tests (add --target native|js)
moon run cmd/main -- "2+2" # expression runner
./tools/run_diff.sh tools/corpus.txt # differential check vs Jexl
./tools/run_diff.sh tools/corpus_ctx.txt # JSON-context corpusInstall
Download zipmbel — a MoonBit expression language: Jexl-compatible dynamic evaluation with an expr-lang syntax front-end, dual tree-walk/bytecode-VM engines, predicate aggregates and resource budgets.