subscript

    7.3.1 • Public • Published

    subscript

    Subscript is expression evaluator / microlanguage with common syntax.

    • Tiny size npm bundle size
    • 🚀 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
    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

    Extending

    Operators/tokens can be extended via:

    • unary(str, prec, postfix=false) − register unary operator, either prefix or postfix.
    • binary(str, prec, rightAssoc=false) − register binary operator, optionally right-associative.
    • nary(str, prec, allowSkip=false) − register n-ary (sequence) operator, optionally allowing skipping args.
    • token(str, prec, fn) − register custom token or literal. fn takes last token as argument and returns calltree node.
    • operator(str, fn) − register evaluator for operator. fn takes node arguments and returns evaluator function.
    import script, { operator, unary, binary, token } from './subscript.js'
    
    // add ~ unary operator with precedence 15
    unary('~', 15)
    operator('~', a => ~a)
    
    // add === binary operator with precedence 9
    binary('===', 9)
    operator('===', (a, b) => a===b)
    
    // add literals
    token('true', 20, a => ['',true])
    token('false', 20, a => ['',false])
    operator('', a => ctx => a[1]])

    See subscript.js or justin.js for examples.

    Syntax tree

    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, 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"

    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

    🕉

    Install

    npm i subscript

    DownloadsWeekly Downloads

    12

    Version

    7.3.1

    License

    ISC

    Unpacked Size

    31.8 kB

    Total Files

    9

    Last publish

    Collaborators

    • tmcw
    • dy