Subscript is fast, tiny & extensible parser / evaluator / microlanguage.
- expressions evaluators, calculators
- subsets of languages
- sandboxes, playgrounds, safe eval
- custom DSL
- preprocessors
- templates
Subscript has 3.5kb footprint , good 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 })
// 3Subscript supports common syntax (JavaScript, C, C++, Java, C#, PHP, Swift, Objective-C, Kotlin, Perl etc.):
a.b,a[b],a(b)a++,a--,++a,--aa * b,a / b,a % b+a,-a,a + b,a - ba < 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 || ba = 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 !== ba ** b,a **= ba ?? b,a ??= ba ||= b,a &&= ba >>> b,a >>>= ba ? b : c,a?.b...a[a, b]{a: b}(a, b) => c// foo,/* bar */true,false,null,NaN,undefineda in b
import justin from 'subscript/justin'
let fn = justin('{ x: 1, "y": 2+2 }["x"]')
fn() // 1if (c) a,if (c) a else bwhile (c) bodyfor (init; cond; step) body{ a; b }— block scopelet x,const x = 1break,continue,return x`a ${x} b`— template literals/pattern/flags— regex literals5px,10rem— unit suffixes
import subscript from 'subscript/justin'
import 'subscript/feature/loop.js'
let sum = subscript(`
let sum = 0;
for (i = 0; i < 10; i += 1) sum += i;
sum
`)
sum() // 45Subscript 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 }) // 2AST 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' // identifier — variable from scope
[, value] // literal — [0] empty distinguishes from operator
[op, a] // unary — prefix operator
[op, a, null] // unary — postfix operator (null marks postfix)
[op, a, b] // binary
[op, a, b, c] // n-ary / ternary
// operators
['+', a, b] // a + b
['.', a, 'b'] // a.b — property access
['[]', a, b] // a[b] — bracket access
['()', a] // (a) — grouping
['()', a, b] // a(b) — function call
['()', a, null] // a() — call with no args
// literals & structures
[, 1] // 1
[, 'hello'] // "hello"
['[]', [',', ...]] // [a, b] — array literal
['{}', [':', ...]] // {a: b} — object literal
// justin extensions
['?', a, b, c] // a ? b : c — ternary
['=>', params, x] // (a) => x — arrow function
['...', a] // ...a — spread
// control flow (extra)
['if', cond, then, else]
['while', cond, body]
['for', init, cond, step, body]
// postfix example
['++', 'a'] // ++a
['++', 'a', null] // a++
['px', [,5]] // 5px (unit suffix)To convert tree back to code, there's codegenerator function:
import { stringify } from 'subscript.js'
stringify(['+', ['*', 'min', [,60]], [,'sec']])
// 'min * 60 + "sec"'Subscript provides premade language features and API to customize syntax:
unary(str, precedence, postfix=false)− register unary operator, either prefix⚬aor 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