Subscript is fast, tiny & extensible expression evaluator / microlanguage with standard syntax.
- templates (eg. sprae)
- expressions evaluators, calculators
- subsets of languages (eg. justin)
- sandboxes, playgrounds, safe eval
- custom DSL (eg. piezo)
- preprocessors (eg. prepr)
Subscript has 3.5kb footprint (compare to 11.4kb jsep + 4.5kb expression-eval), best performance and extensive test coverage.
import subscript from './subscript.js'
// parse expression
const fn = subscript('a.b + Math.sqrt(c - 1)')
// evaluate with context
fn({ a: { b:1 }, c: 5, Math })
// 3
Subscript supports common syntax (JavaScript, C, C++, Java, C#, PHP, Swift, Objective-C, Kotlin, Perl etc.):
a.b
,a[b]
,a(b)
a++
,a--
,++a
,--a
a * b
,a / b
,a % b
+a
,-a
,a + b
,a - b
a < b
,a <= b
,a > b
,a >= b
,a == b
,a != b
~a
,a & b
,a ^ b
,a | b
,a << b
,a >> b
!a
,a && b
,a || b
a = b
,a += b
,a -= b
,a *= b
,a /= b
,a %= b
, ,a <<= b
,a >>= b
(a, (b))
,a; b;
"abc"
,'abc'
0.1
,1.2e+3
Just-in is no-keywords JS subset, JSON + expressions (see thread).
It extends subscript with:
a === b
,a !== b
a ** b
,a **= b
a ?? b
,a ??= b
a ||= b
,a &&= b
a >>> b
,a >>>= b
a ? b : c
,a?.b
...a
[a, b]
{a: b}
(a, b) => c
// foo
,/* bar */
true
,false
,null
,NaN
,undefined
a in b
import jstin from './justin.js'
let xy = jstin('{ x: 1, "y": 2+2 }["x"]')
xy() // 1
Subscript exposes parse
to build AST and compile
to create evaluators.
import { parse, compile } from 'subscript'
// parse expression
let tree = parse('a.b + c - 1')
tree // ['-', ['+', ['.', 'a', 'b'], 'c'], [,1]]
// compile tree to evaluable function
fn = compile(tree)
fn({ a: {b: 1}, c: 2 }) // 3
AST has simplified lispy tree structure (inspired by frisk / nisp), opposed to ESTree:
- not limited to particular language (JS), can be compiled to different targets;
- reflects execution sequence, rather than code layout;
- has minimal 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"
// node kinds
['+', a]; // unary operator `+a`
['+', a, b]; // binary operator `a + b`
['+', a, b, c]; // n-ary operator `a + b + c`
['()', a]; // group operator `(a)`
['()', a, b]; // access operator `a(b)`
[, a]; // literal value `'a'`
a; // variable (from scope)
null; // placeholder
// eg.
['()', 'a'] // (a)
['()', 'a', null] // a()
To convert tree back to code, there's codegenerator function:
import { stringify } from 'subscript.js'
stringify(['+', ['*', 'min', [,60]], [,'sec']])
// 'min*60 + "sec" == "300sec"'
Subscript provides premade language features and API to customize syntax:
unary(str, precedence, postfix=false)
− register unary operator, either prefix⚬a
or postfixa⚬
.binary(str, precedence, rassoc=false)
− register binary operatora ⚬ b
, optionally right-associative.nary(str, precedence)
− register n-ary (sequence) operator likea; b;
ora, b
, allows missing args.group(str, precedence)
- register group, like[a]
,{a}
,(a)
etc.access(str, precedence)
- register access operator, likea[b]
,a(b)
etc.token(str, precedence, lnode => node)
− register custom token or literal. Callback takes left-side node and returns complete expression node.operator(str, (a, b) => ctx => value)
− register evaluator for an operator. Callback takes node arguments and returns evaluator function.
Longer operators should be registered after shorter ones, eg. first |
, then ||
, then ||=
.
import script, { compile, operator, unary, binary, token } from './subscript.js'
// enable objects/arrays syntax
import 'subscript/feature/array.js';
import 'subscript/feature/object.js';
// add identity operators (precedence of comparison)
binary('===', 9), binary('!==', 9)
operator('===', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)===b(ctx)))
operator('===', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)!==b(ctx)))
// add nullish coalescing (precedence of logical or)
binary('??', 3)
operator('??', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) ?? b(ctx)))
// add JS literals
token('undefined', 20, a => a ? err() : [, undefined])
token('NaN', 20, a => a ? err() : [, NaN])
See ./feature/*
or ./justin.js
for examples.
Subscript shows 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:
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: -
jexpr, jsep, jexl, mozjexl, expr-eval, expression-eval, string-math, nerdamer, math-codegen, math-parser, math.js, nx-compile, built-in-math-eval