subscript
subscript copied to clipboard
Fast and tiny expression evaluator with minimal syntax.
Subscript is expression evaluator / microlanguage with common expressions syntax (JavaScript, Java, C, C++, Rust, Go, Python, Kotlin etc).
import script, { parse, compile } from './subscript.js'
// create expression evaluator
let fn = script('a.b + c(d - 1)')
fn({ a: { b:1 }, c: x => x * 2, d: 3 }) // 5
// or
// parse expression
let tree = parse('a.b + c')
tree // ['+', ['.', 'a', 'b'], 'c']
// compile tree to evaluable function
let evaluate = compile(tree)
Motivation
Subscript is designed to be useful for:
- templates (perfect match with template parts, see templize)
- expressions evaluators, calculators
- configurable subsets of languages (eg. justin)
- pluggable/mock language features (eg. pipe operator)
- sandboxes, playgrounds, safe eval
- custom DSL
Subscript has 2.8kb footprint, compared to 11.4kb jsep + 4.5kb expression-eval, with better test coverage and better performance.
Operators
-
( a, b, c )
-
a.b
,a[b]
,a(b, c)
-
a++
,a--
unary postfix -
!a
,+a
,-a
,++a
,--a
unary prefix -
a * b
,a / b
,a % b
-
a + b
,a - b
-
a << b
,a >> b
,a >>> b
-
a < b
,a <= b
,a > b
,a >= b
-
a == b
,a != b
-
a & b
-
a ^ b
-
a | b
-
a && b
-
a || b
-
a , b
Literals
-
"abc"
strings -
1.2e+3
numbers
Extending
Operators/tokens can be extended via:
-
unary(str, precedence, postfix=false)
− register unary operator, either prefix or postfix. -
binary(str, precedence, rightAssoc=false)
− register binary operator, optionally right-associative. -
nary(str, precedence, allowSkip=false)
− register n-ary (sequence) operator, optionally allowing skipping args. -
token(str, precedence, lhs => expr)
− register custom token or literal. Mapper takes last token argument and returns calltree node. -
operator(str, fn)
− register evaluator for operator.fn
takes node arguments and returns evaluator function.
import script, { operator, unary, binary, token } from './subscript.js'
// add ~ unary operator with precedence 15
unary('~', 15)
operator('~', a => ~a)
// add === binary operator with precedence 9
binary('===', 9)
operator('===', (a, b) => a===b)
// add literals
token('true', 20, a => ['',true])
token('false', 20, a => ['',false])
operator('', a => ctx => a[1]])
See subscript.js or justin.js for examples.
Syntax tree
Subscript exposes separate ./parse.js
and ./compile.js
entries. Parser builds AST, compiler converts it to evaluable function.
AST has simplified lispy calltree structure (inspired by frisk), opposed to ESTree:
- is not limited to particular language, can be compiled to different targets;
- reflects execution sequence, rather than code layout;
- has minimal possible overhead, directly maps to operators;
- simplifies manual evaluation and debugging;
- has conventional form and one-liner docs:
import { compile } from 'subscript.js'
const fn = compile(['+', ['*', 'min', ['',60]], ['','sec']])
fn({min: 5}) // min*60 + "sec" == "300sec"
Justin
Justin is minimal JS subset − JSON with JS expressions (see original thread).
It extends subscript with:
-
===
,!==
operators -
**
exponentiation operator (right-assoc) -
~
bit inversion operator -
'
strings -
?:
ternary operator -
?.
optional chain operator -
??
nullish coalesce operator -
[...]
Array literal -
{...}
Object literal -
in
binary -
;
expression separator -
//
,/* */
comments -
true
,false
,null
,undefined
literals
import jstin from 'subscript/justin.js'
let xy = jstin('{ x: 1, "y": 2+2 }["x"]')
xy() // 1
Performance
Subscript shows relatively good performance within other evaluators. Example expression:
1 + (a * b / c % d) - 2.0 + -3e-3 * +4.4e4 / f.g[0] - i.j(+k == 1)(0)
Parse 30k times:
es-module-lexer: 50ms 🥇
subscript: ~150 ms 🥈
justin: ~183 ms
jsep: ~270 ms 🥉
jexpr: ~297 ms
mr-parser: ~420 ms
expr-eval: ~480 ms
math-parser: ~570 ms
math-expression-evaluator: ~900ms
jexl: ~1056 ms
mathjs: ~1200 ms
new Function: ~1154 ms
Eval 30k times:
new Function: ~7 ms 🥇
subscript: ~15 ms 🥈
justin: ~17 ms
jexpr: ~23 ms 🥉
jsep (expression-eval): ~30 ms
math-expression-evaluator: ~50ms
expr-eval: ~72 ms
jexl: ~110 ms
mathjs: ~119 ms
mr-parser: -
math-parser: -