
Typed, safe expression language for evaluating guarded predicates: compile-once, tree-walking evaluator with lexer/parser/type-checker, preventing code injection, precise source-linked errors, REPL/CLI and cacheable expressions.
A small, safe, typed expression language for the JVM and the browser, written in Kotlin Multiplatform.
Evaluate expressions like age >= 18 && country in ["UK", "IE"] against a context — with no code
injection, type errors caught before evaluation, and clear messages that point at the source.
Status: working, pre-release. The full pipeline is in with a public
ExprAPI — lexer, parser, static type checker and tree-walking evaluator, all tested, with a REPL/CLI. The same code compiles to JVM and JS (Kotlin Multiplatform), and a browser playground runs the library compiled to JS. Not yet published to Maven Central.
Rule engines, feature-flag conditions and search filters keep needing to "evaluate this little
expression, safely". The usual options (SpEL, MVEL, JEXL) are heavy, reflection-based, or unsafe.
expr4k aims to be small, safe by construction (no arbitrary code execution), typed, and
pleasant to embed.
// compile once (lex + parse + type-check), evaluate many times
val expr = Expr.compile("age >= 18 && country in ['UK', 'IE']")
val ok: Boolean = expr.evalBoolean(mapOf("age" to 21, "country" to "UK")) // truePass a schema to turn type mismatches into compile-time errors instead of runtime ones:
Expr.compile("age && true", mapOf("age" to Type.NUMBER))
// throws TypeException: operator '&&' expects a boolean but got number at 1:1Every failure — lexing, parsing, type checking or evaluation — is an Expr4kException carrying the
source position it refers to. A compiled Expr is immutable and safe to cache and share across
threads.
A small REPL and one-shot evaluator ship alongside the library:
./gradlew runCli --args="2 + 3 * 4" # one-shot → 14.0
./gradlew runCli # REPL
> :let age 21
age = 21.0 : number
> age >= 18 && age < 65
true : boolean
> :help # :let, :type, :vars, :quitA hand-written pipeline, no parser generator — owning the parser is the point:
source ──▶ Lexer ──▶ tokens ──▶ parser ──▶ typed AST ──▶ type check ──▶ evaluator ──▶ value
./gradlew :benchmark:jmh runs the JMH microbenchmarks (in the
:benchmark module — a small JVM module that consumes the library's JVM target). Compiling a
predicate once and reusing it is far cheaper than re-parsing on every evaluation:
| approach | ns / eval | throughput |
|---|---|---|
| compile once, eval many | ~95 | ~10.5M / sec |
| compile on every eval | ~1,590 | ~0.63M / sec |
So reusing a compiled Expr is roughly 17x faster per evaluation (JMH average time, 2 forks × 5
iterations). Figures are machine-dependent — run it yourself.
./gradlew buildRequires a JDK; the Gradle wrapper fetches the rest.
MIT.
A small, safe, typed expression language for the JVM and the browser, written in Kotlin Multiplatform.
Evaluate expressions like age >= 18 && country in ["UK", "IE"] against a context — with no code
injection, type errors caught before evaluation, and clear messages that point at the source.
Status: working, pre-release. The full pipeline is in with a public
ExprAPI — lexer, parser, static type checker and tree-walking evaluator, all tested, with a REPL/CLI. The same code compiles to JVM and JS (Kotlin Multiplatform), and a browser playground runs the library compiled to JS. Not yet published to Maven Central.
Rule engines, feature-flag conditions and search filters keep needing to "evaluate this little
expression, safely". The usual options (SpEL, MVEL, JEXL) are heavy, reflection-based, or unsafe.
expr4k aims to be small, safe by construction (no arbitrary code execution), typed, and
pleasant to embed.
// compile once (lex + parse + type-check), evaluate many times
val expr = Expr.compile("age >= 18 && country in ['UK', 'IE']")
val ok: Boolean = expr.evalBoolean(mapOf("age" to 21, "country" to "UK")) // truePass a schema to turn type mismatches into compile-time errors instead of runtime ones:
Expr.compile("age && true", mapOf("age" to Type.NUMBER))
// throws TypeException: operator '&&' expects a boolean but got number at 1:1Every failure — lexing, parsing, type checking or evaluation — is an Expr4kException carrying the
source position it refers to. A compiled Expr is immutable and safe to cache and share across
threads.
A small REPL and one-shot evaluator ship alongside the library:
./gradlew runCli --args="2 + 3 * 4" # one-shot → 14.0
./gradlew runCli # REPL
> :let age 21
age = 21.0 : number
> age >= 18 && age < 65
true : boolean
> :help # :let, :type, :vars, :quitA hand-written pipeline, no parser generator — owning the parser is the point:
source ──▶ Lexer ──▶ tokens ──▶ parser ──▶ typed AST ──▶ type check ──▶ evaluator ──▶ value
./gradlew :benchmark:jmh runs the JMH microbenchmarks (in the
:benchmark module — a small JVM module that consumes the library's JVM target). Compiling a
predicate once and reusing it is far cheaper than re-parsing on every evaluation:
| approach | ns / eval | throughput |
|---|---|---|
| compile once, eval many | ~95 | ~10.5M / sec |
| compile on every eval | ~1,590 | ~0.63M / sec |
So reusing a compiled Expr is roughly 17x faster per evaluation (JMH average time, 2 forks × 5
iterations). Figures are machine-dependent — run it yourself.
./gradlew buildRequires a JDK; the Gradle wrapper fetches the rest.
MIT.