Tiny expression parser & evaluator.
- Safe — sandboxed, blocks
__proto__,constructor, no global access - Fast — Pratt parser engine, see benchmarks
- Portable — universal expression format, any compile target
- Metacircular — can parse and compile itself
- Extensible — pluggable syntax for building custom DSL
import subscript from 'subscript'
let fn = subscript('a + b * 2')
fn({ a: 1, b: 3 }) // 7Common expressions:
a.b a[b] a(b) + - * / % < > <= >= == != ! && || ~ & | ^ << >> ++ -- = += -= *= /=
import subscript from 'subscript'
subscript('a.b + c * 2')({ a: { b: 1 }, c: 3 }) // 7JSON + expressions + templates + arrows:
'str' 0x 0b === !== ** ?? >>> ?. ? : => ... [] {} ` // /**/ true false null
import justin from 'subscript/justin.js'
justin('{ x: a?.b ?? 0, y: [1, ...rest] }')({ a: null, rest: [2, 3] })
// { x: 0, y: [1, 2, 3] }JSON + expressions + statements, functions:
if else for while do let const var function class return throw try catch switch import export /regex/
import jessie from 'subscript/jessie.js'
let fn = jessie(`
function factorial(n) {
if (n <= 1) return 1
return n * factorial(n - 1)
}
factorial(5)
`)
fn({}) // 120Jessie can parse and compile its own source.
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 }) // 2import { binary, operator, compile } from 'subscript/justin.js'
// add intersection operator
binary('∩', 80) // register parser
operator('∩', (a, b) => ( // register compiler
a = compile(a), b = compile(b),
ctx => a(ctx).filter(x => b(ctx).includes(x))
))import justin from 'subscript/justin.js'
justin('[1,2,3] ∩ [2,3,4]')({}) // [2, 3]See docs.md for full API.
Expressions parse to a minimal JSON-compatible AST:
import { parse } from 'subscript'
parse('a + b * 2')
// ['+', 'a', ['*', 'b', [, 2]]]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:
Three forms:
'x' // identifier — resolve from context
[, value] // literal — return as-is (empty slot = data)
[op, ...args] // operation — apply operatorSee spec.md.
Blocked by default:
__proto__,__defineGetter__,__defineSetter__constructor,prototype- Global access (only context is visible)
subscript('constructor.constructor("alert(1)")()')({})
// undefined (blocked)Parse 30k: subscript 150ms · justin 183ms · jsep 270ms · expr-eval 480ms · jexl 1056ms
Eval 30k: new Function 7ms · subscript 15ms · jsep+eval 30ms · expr-eval 72ms
Convert tree back to code:
import { codegen } from 'subscript/util/stringify.js'
codegen(['+', ['*', 'min', [,60]], [,'sec']])
// 'min * 60 + "sec"'Create custom dialect as single file:
import { bundle } from 'subscript/util/bundle.js'
const code = await bundle('subscript/jessie.js')
// → self-contained ES module- jz — JS subset → WASM compiler