subscript

Fast and tiny expression evaluator with common syntax microlanguage.

Usage no npm install needed!

<script type="module">
  import subscript from 'https://cdn.skypack.dev/subscript';
</script>

README

subscript

Subscript is expression evaluator / microlanguage with common syntax.

  • Any fragment can be copy-pasted to any language: C++, JS, Java, Python, Go, Rust etc.
  • Tiny size npm bundle size
  • :rocket: Fast performance
  • Configurable & extensible
  • Trivial to use
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 tree
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.

Design

Default operators are (same as JS precedence order):

  • ( 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

Default literals:

  • "abc" strings
  • 1.2e+3 numbers

Everything else can be extended via subscript.set(str, prec, fn) for unary, binary or n-ary operators (detected by number of arguments in fn), or via subscript.set(str, prec, [parse, compile]) for custom tokens.

import script, { compile } from './subscript.js'

// add ~ unary operator with precedence 15
script.set('~', 15, a => ~a)

// add === binary operator with precedence 9
script.set('===', 9, (a, b) => a===b)

// add literals
script.set('true', 20, [a => ['',true], a => ctx => a[1]])
script.set('false', 20, [a => ['',false], a => ctx => a[1]])

script(`true === false`)() // false

See subscript.js or justin.js for examples.

Parser & Compiler

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 cross-compiled;
  • reflects execution sequence, rather than code layout;
  • has minimal possible overhead, better fits for directly mapping 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:

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: -

Alternatives

JS engines

🕉