JSPM

  • Created
  • Published
  • Downloads 7226
  • Score
    100M100P100Q128130F
  • License ISC

Fast and tiny expression evaluator with minimal syntax.

Package Exports

  • subscript
  • subscript/compile.js
  • subscript/justin.js
  • subscript/parse.js
  • subscript/subscript.js

Readme

subscript npm bundle size

Subscript is fast, tiny & extensible expression evaluator / microlanguage.

Used for:

  • templates (eg. sprae, templize)
  • expressions evaluators, calculators
  • subsets of languages (eg. justin)
  • sandboxes, playgrounds, safe eval
  • custom DSL (eg. mell)
  • preprocessors (eg. prepr)

Subscript has 3.5kb footprint (compare to 11.4kb jsep + 4.5kb expression-eval), good performance and wide test coverage.

Usage

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

Operators

Subscript supports common syntax (shared by 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;
  • "abc", 'abc'
  • 0.1, 1.2e+3

Justin

Justin is minimal JS subset, JSON + Expressions (see thread). It extends subscript with:

  • a ** b (right-assoc)
  • a ? b : c
  • a?.b
  • [a, b] Array
  • {a: b} Object
  • a in b
  • // foo, /* bar */
  • true, false, null
import jstin from './justin.js'

let xy = jstin('{ x: 1, "y": 2+2 }["x"]')
xy()  // 1

Parse / Compile

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

Syntax Tree

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-line docs:
import { compile } from 'subscript.js'

const fn = compile(['+', ['*', 'min', [,60]], [,'sec']])

fn({min: 5}) // min*60 + "sec" == "300sec"

Extending

Subscript provides API to customize or extend syntax:

  • 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, prevNode => curNode) − register custom token or literal. Function takes last token and returns tree node.
  • operator(str, (a, b) => ctx => result) − register evaluator for an operator. Function takes node arguments and returns evaluator function.
import script, { compile, operator, unary, binary, token } from './subscript.js'

// add identity operators with precedence 9
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 JS literals
token('undefined', 20, a => a ? err() : [, undefined])
token('NaN', 20, a => a ? err() : [, NaN])

See ./feature/* for examples.

Performance

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

Alternatives

🕉