import subscript from 'subscript'
// compile and evaluate
let fn = subscript('x + 1')
fn({ x: 2 }) // 3
// or one-liner
subscript('a * b')({ a: 3, b: 4 }) // 12subscript('a + b')({ a: 1, b: 2 }) // 3Parses and compiles on each call. Use for dynamic expressions.
subscript`a + b`({ a: 1, b: 2 }) // 3
// same template reuses compiled function
const check = () => subscript`x > 0`
check() === check() // true (cached)
// embed values
const limit = 10
subscript`x > ${limit}`({ x: 15 }) // trueconst double = x => x * 2
subscript`${double}(x)`({ x: 5 }) // 10
// embed objects
const math = { pi: 3.14 }
subscript`${math}.pi * r * r`({ r: 2 }) // 12.56
// embed AST nodes
subscript`${['+', 'a', 'b']} * 2`({ a: 1, b: 2 }) // 6Detection rules:
| Value | Detected As | Result |
|---|---|---|
5, "hi", true |
Primitive | Literal [, value] |
{a: 1} |
Object | Literal [, obj] |
x => x * 2 |
Function | Literal [, fn] |
[1, 2, 3] |
Array (number first) | Literal [, arr] |
'varName' |
String | Identifier (AST) |
['+', 'a', 'b'] |
Array (string first) | AST node |
[, 100] |
Array (undefined first) | AST literal |
import { parse, compile } from 'subscript'
let ast = parse('a + b')
// ['+', 'a', 'b']
let fn = compile(ast)
fn({ a: 1, b: 2 }) // 3Minimal common syntax for JavaScript, C, C++, Java, C#, PHP, Swift, Objective-C, Kotlin, Perl etc.:
a.b,a[b],a(b)— property accessa * b,a / b,a % b— multiplicative+a,-a,a + b,a - b— additivea < b,a <= b,a > b,a >= b— comparisona == b,a != b— equality!a,a && b,a || b— logical~a,a | b,a & b,a ^ b,a << b,a >> b— bitwisea++,a--,++a,--a— increment/decrementa = b,a += b,a -= b,a *= b,a /= b,a %= b— assignmenta |= b,a &= b,a ^= b,a >>= b,a <<= b— bitwise assignmenta, b,a; b— sequences"abc"— double-quoted strings0.1,1.2e3— decimal numbers
import subscript from 'subscript'
subscript('a.b + c * 2')({ a: { b: 1 }, c: 3 }) // 7Just-in is no-keywords JS subset, JSON + expressions (thread) — extends subscript with:
'abc'— single-quoted strings0x1f,0b11,0o17— hex, binary, octala === b,a !== b— strict equalitya ** b,a **= b— exponentiationa ?? b,a ??= b— nullish coalescinga >>> b,a >>>= b,a ||= b,a &&= b— JS-specific operatorsa?.b,a?.[b],a?.(b)— optional chaininga ? b : c— ternarya in b— membership...a— spread[a, b],{a: b},{a}— collections(a, b) => c— arrow functions`a ${x} b`,tag`...`— templates// foo,/* bar */— commentstrue,false,null,undefined,NaN,Infinity— literals
import justin from 'subscript/justin.js'
justin`{ x: 1, y: 2+2 }.x`({}) // 1Extends Justin with statements — practical JS subset (inspired by Jessie).
if (c) a,if (c) a else bwhile (c) body,do { body } while (c)for (init; cond; step) body,for (x of iter) body,for (x in obj) body{ a; b }— block scopelet x,const x = 1,var x = 1,const {a, b} = xbreak,continue,return x(inside functions only)throw x,try { } catch (e) { } finally { }function f(a, b) { },async function,function*,await,yieldclass X { },class X extends Y { }typeof x,void x,delete x,x instanceof Ynew X(),new X(a, b)import,exportswitch (x) { case a: ...; default: ... }{ get x() {}, set y(v) {} }/pattern/flags— regex
import jessie from 'subscript/jessie.js'
jessie`
function fac(n) {
if (n <= 1) return 1;
return n * fac(n - 1)
};
fac(5)
`({}) // 120
// Top-level return is invalid (same as JS) — use expressions:
jessie`a + b`({ a: 1, b: 2 }) // 3Async functions return promises. await works in return position:
// Works: await in return
jessie`async function f() { return await Promise.resolve(42) }`({ Promise })
// Limitation: await in assignment returns Promise, not value
// let x = await y; x * 2 → NaN (x is Promise)Jessie supports JS-style ASI – newlines at statement level act as semicolons:
jessie('a = 1\nb = 2\na + b')({}) // 3ASI precedence can be customized via prec.asi:
import { prec } from 'subscript/parse.js'
prec.asi = 0 // disable ASI (require explicit semicolons)
prec.asi = 150 // ASI even inside expressions (newline always separates)
delete prec.asi // restore default (prec[';'])
import 'subscript/feature/asi.jsBinary operator a ⚬ b, optionally right-associative.
import { binary } from 'subscript'
binary('%%', 110) // modulo precedence
// parses: a %% b → ['%%', 'a', 'b']Unary operator, either prefix ⚬a or postfix a⚬
import { unary } from 'subscript'
unary('√', 150) // prefix: √x
unary('°', 150, true) // postfix: x°N-ary (sequence) operator like a; b; or a, b, allows empty slots.
import { nary } from 'subscript'
nary(';', 10, true) // a; b; c → [';', 'a', 'b', 'c']Group construct, like [a], {a} etc.
import { group } from 'subscript'
group('()', 200) // (a) → ['()', 'a']
group('[]', 200) // [a] → ['[]', 'a']Member access operator, like a[b], a(b) etc.
import { access } from 'subscript'
access('()', 180) // a(b) → ['()', 'a', 'b']
access('[]', 180) // a[b] → ['[]', 'a', 'b']Keyword literals:
import { literal } from 'subscript'
literal('nil', null)
literal('yes', true)Low-level token handler:
import { token } from 'subscript'
// Ternary: a ? b : c
token('?', 25, left => {
if (!left) return // needs left operand
const then = expr(0)
skip() // skip ':'
const els = expr(24)
return ['?', left, then, els]
})Note
When extending operators that share a prefix (like =, ==, ===), import shorter operators first so longer ones are checked first in the token chain.
This applies to: =/==/===, !/!=/!==, |/||, &/&&, etc.
Register evaluator for an operator.
import { operator, compile } from 'subscript'
operator('%%', (a, b) => {
const ea = compile(a), eb = compile(b)
return ctx => ((ea(ctx) % eb(ctx)) + eb(ctx)) % eb(ctx)
})
subscript`-5 %% 3`() // 1 (true modulo)Registry object. Override or inspect:
import { operators } from 'subscript'
operators['+'] // current handlerHelper for property access patterns:
import { prop, compile } from 'subscript'
operator('delete', a => prop(a, (obj, key) => delete obj[key]))import {
// Parser
parse, // parse(code) → AST
token, // define token
binary, // define binary op
unary, // define unary op
nary, // define sequence op
group, // define grouping
access, // define accessor
literal, // define literal
// Compiler
compile, // compile(ast) → fn
operator, // register op handler
operators, // op registry
prop, // prop access helper
codegen, // ast → source string
} from 'subscript'AST has simplified lispy tree structure (inspired by frisk / nisp), opposed to ESTree:
- portable to any language, not limited to JS;
- 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:
'x' // identifier
[, 1] // literal
['+', 'a', 'b'] // binary
['-', 'a'] // unary prefix
['++', 'a', null] // unary postfix
['?', a, b, c] // ternary
['if', cond, then, else] // control flowSee full spec