Skip to content

Commit f9a16a0

Browse files
authored
Merge pull request #28 from dy/pivot
Pivot
2 parents b37633a + fc2a852 commit f9a16a0

131 files changed

Lines changed: 5227 additions & 7320 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Avoid writing to `lookup`, use token instead.
2+
Avoid ad-hocs - that signals inconsistency of design. High-level API is for a reason, we need to use it and if it doesn't fit, we need to improve it.
3+
`/features` represent generic language features, not only JS: avoid js-specific checks.
4+
For features you implement or any changes, if relevant please add tests, update spec.md, docs.md and README.md, as well as REPL and other relevant files. Make sure tests pass.
5+
Make sure API and feature code is intuitive and user-friendly: prefer `unary`/`binary`/`nary`/`group`/`token` calls in the right order ( eg. first `|`, then `||`, then `||=` ) rather than low-level parsing. Eg. feature should not use `cur`, `idx` and use `skip` instead.
6+
The project is planned to be built with jz - simple javascript subset compiling to wasm, so don't use complex structures like Proxy, classes etc.
7+
By introducing a change, think how would that scale to various dialects and compile targets. Also make sure it doesn't compromise performance and doesn't bloat the code. Justin must be faster than jsep.
8+
By writing parser feature, aim for raw tree shape parsed with minimal code rather than edge cases validation.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ dist
106106
# TernJS port file
107107
.tern-port
108108
.DS_Store
109+
test-results/

README.md

Lines changed: 115 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,67 @@
1-
# sub<em>script</em> <a href="https://github.com/spectjs/subscript/actions/workflows/node.js.yml"><img src="https://github.com/spectjs/subscript/actions/workflows/node.js.yml/badge.svg"/></a> <a href="https://bundlejs.com/?q=subscript"><img alt="npm bundle size" src="https://img.shields.io/bundlejs/size/subscript"/></a> <a href="http://npmjs.org/subscript"><img src="https://img.shields.io/npm/v/subscript"/></a> <a href="http://microjs.com/#subscript"><img src="https://img.shields.io/badge/microjs-subscript-blue?color=darkslateblue"/></a>
1+
# sub<sub><sup> ✦ </sup></sub>script [![build](https://github.com/dy/subscript/actions/workflows/node.js.yml/badge.svg)](https://github.com/dy/subscript/actions/workflows/node.js.yml) [![npm](https://img.shields.io/npm/v/subscript)](http://npmjs.org/subscript) [![size](https://img.shields.io/bundlephobia/minzip/subscript?label=size)](https://bundlephobia.com/package/subscript) [![demo](https://img.shields.io/badge/play-%F0%9F%9A%80-white)](https://dy.github.io/subscript/) [![microjs](https://img.shields.io/badge/microjs-subscript-blue?color=darkslateblue)](http://microjs.com/#subscript)
22

3-
> _Subscript_ is fast, tiny & extensible parser / evaluator / microlanguage.
3+
> Tiny expression parser & evaluator.
44
5-
#### Used for:
5+
* **Safe** — sandboxed, blocks `__proto__`, `constructor`, no global access
6+
* **Fast** — Pratt parser engine, see [benchmarks](#performance)
7+
* **Portable** — universal expression format, any compile target
8+
* **Metacircular** — can parse and compile itself
9+
* **Extensible** — pluggable syntax for building custom DSL
610

7-
* expressions evaluators, calculators
8-
* subsets of languages
9-
* sandboxes, playgrounds, safe eval
10-
* custom DSL
11-
* preprocessors
12-
* templates
11+
## Usage
1312

14-
_Subscript_ has [3.5kb](https://npmfs.com/package/subscript/7.4.3/subscript.min.js) footprint <!-- (compare to [11.4kb](https://npmfs.com/package/jsep/1.2.0/dist/jsep.min.js) _jsep_ + [4.5kb](https://npmfs.com/package/expression-eval/5.0.0/dist/expression-eval.module.js) _expression-eval_) -->, good [performance](#performance) and extensive test coverage.
13+
```js
14+
import subscript from 'subscript'
1515

16+
let fn = subscript('a + b * 2')
17+
fn({ a: 1, b: 3 }) // 7
18+
```
1619

17-
## Usage
20+
## Presets
1821

19-
```js
20-
import subscript from './subscript.js'
22+
### subscript
2123

22-
// parse expression
23-
const fn = subscript('a.b + Math.sqrt(c - 1)')
24+
Common expressions:
2425

25-
// evaluate with context
26-
fn({ a: { b:1 }, c: 5, Math })
27-
// 3
26+
`a.b a[b] a(b) + - * / % < > <= >= == != ! && || ~ & | ^ << >> ++ -- = += -= *= /=`
27+
```js
28+
import subscript from 'subscript'
29+
30+
subscript('a.b + c * 2')({ a: { b: 1 }, c: 3 }) // 7
2831
```
2932

30-
## Operators
31-
32-
_Subscript_ supports [common syntax](https://en.wikipedia.org/wiki/Comparison_of_programming_languages_(syntax)) (_JavaScript_, _C_, _C++_, _Java_, _C#_, _PHP_, _Swift_, _Objective-C_, _Kotlin_, _Perl_ etc.):
33-
34-
* `a.b`, `a[b]`, `a(b)`
35-
* `a++`, `a--`, `++a`, `--a`
36-
* `a * b`, `a / b`, `a % b`
37-
* `+a`, `-a`, `a + b`, `a - b`
38-
* `a < b`, `a <= b`, `a > b`, `a >= b`, `a == b`, `a != b`
39-
* `~a`, `a & b`, `a ^ b`, `a | b`, `a << b`, `a >> b`
40-
* `!a`, `a && b`, `a || b`
41-
* `a = b`, `a += b`, `a -= b`, `a *= b`, `a /= b`, `a %= b`, `a <<= b`, `a >>= b`
42-
* `(a, (b))`, `a; b;`
43-
* `"abc"`, `'abc'`
44-
* `0.1`, `1.2e+3`
45-
46-
### Justin
47-
48-
_Just-in_ is no-keywords JS subset, _JSON_ + _expressions_ (see [thread](https://github.com/endojs/Jessie/issues/66)).<br/>
49-
It extends _subscript_ with:
50-
51-
+ `a === b`, `a !== b`
52-
+ `a ** b`, `a **= b`
53-
+ `a ?? b`, `a ??= b`
54-
+ `a ||= b`, `a &&= b`
55-
+ `a >>> b`, `a >>>= b`
56-
+ `a ? b : c`, `a?.b`
57-
+ `...a`
58-
+ `[a, b]`
59-
+ `{a: b}`
60-
+ `(a, b) => c`
61-
+ `// foo`, `/* bar */`
62-
+ `true`, `false`, `null`, `NaN`, `undefined`
63-
+ `a in b`
33+
### justin
34+
35+
JSON + expressions + templates + arrows:
6436

37+
`` 'str' 0x 0b === !== ** ?? >>> ?. ? : => ... [] {} ` // /**/ true false null ``
6538
```js
66-
import justin from 'subscript/justin'
39+
import justin from 'subscript/justin.js'
6740

68-
let fn = justin('{ x: 1, "y": 2+2 }["x"]')
69-
fn() // 1
41+
justin('{ x: a?.b ?? 0, y: [1, ...rest] }')({ a: null, rest: [2, 3] })
42+
// { x: 0, y: [1, 2, 3] }
7043
```
7144

72-
### Extra
45+
### jessie
7346

74-
+ `if (c) a`, `if (c) a else b`
75-
+ `while (c) body`
76-
+ `for (init; cond; step) body`
77-
+ `{ a; b }` — block scope
78-
+ `let x`, `const x = 1`
79-
+ `break`, `continue`, `return x`
80-
+ `` `a ${x} b` `` — template literals
81-
+ `/pattern/flags` — regex literals
82-
+ `5px`, `10rem` — unit suffixes
47+
JSON + expressions + statements, functions:
8348

49+
`if else for while do let const var function class return throw try catch switch import export /regex/`
8450
```js
85-
import subscript from 'subscript/justin'
86-
import 'subscript/feature/loop.js'
87-
88-
let sum = subscript(`
89-
let sum = 0;
90-
for (i = 0; i < 10; i += 1) sum += i;
91-
sum
51+
import jessie from 'subscript/jessie.js'
52+
53+
let fn = jessie(`
54+
function factorial(n) {
55+
if (n <= 1) return 1
56+
return n * factorial(n - 1)
57+
}
58+
factorial(5)
9259
`)
93-
sum() // 45
60+
fn({}) // 120
9461
```
9562

63+
Jessie can parse and compile its own source.
64+
9665

9766
## Parse / Compile
9867

@@ -110,157 +79,113 @@ fn = compile(tree)
11079
fn({ a: {b: 1}, c: 2 }) // 2
11180
```
11281

113-
### Syntax Tree
114-
115-
AST has simplified lispy tree structure (inspired by [frisk](https://ghub.io/frisk) / [nisp](https://github.com/ysmood/nisp)), opposed to [ESTree](https://github.com/estree/estree):
82+
## Extension
11683

117-
* not limited to particular language (JS), can be compiled to different targets;
118-
* reflects execution sequence, rather than code layout;
119-
* has minimal overhead, directly maps to operators;
120-
* simplifies manual evaluation and debugging;
121-
* has conventional form and one-liner docs:
84+
```js
85+
import { binary, operator, compile } from 'subscript/justin.js'
86+
87+
// add intersection operator
88+
binary('', 80) // register parser
89+
operator('', (a, b) => ( // register compiler
90+
a = compile(a), b = compile(b),
91+
ctx => a(ctx).filter(x => b(ctx).includes(x))
92+
))
93+
```
12294

12395
```js
124-
import { compile } from 'subscript.js'
125-
126-
const fn = compile(['+', ['*', 'min', [,60]], [,'sec']])
127-
fn({min: 5}) // min*60 + "sec" == "300sec"
128-
129-
// node kinds
130-
'a' // identifier — variable from scope
131-
[, value] // literal — [0] empty distinguishes from operator
132-
[op, a] // unary — prefix operator
133-
[op, a, null] // unary — postfix operator (null marks postfix)
134-
[op, a, b] // binary
135-
[op, a, b, c] // n-ary / ternary
136-
137-
// operators
138-
['+', a, b] // a + b
139-
['.', a, 'b'] // a.b — property access
140-
['[]', a, b] // a[b] — bracket access
141-
['()', a] // (a) — grouping
142-
['()', a, b] // a(b) — function call
143-
['()', a, null] // a() — call with no args
144-
145-
// literals & structures
146-
[, 1] // 1
147-
[, 'hello'] // "hello"
148-
['[]', [',', ...]] // [a, b] — array literal
149-
['{}', [':', ...]] // {a: b} — object literal
150-
151-
// justin extensions
152-
['?', a, b, c] // a ? b : c — ternary
153-
['=>', params, x] // (a) => x — arrow function
154-
['...', a] // ...a — spread
155-
156-
// control flow (extra)
157-
['if', cond, then, else]
158-
['while', cond, body]
159-
['for', init, cond, step, body]
160-
161-
// postfix example
162-
['++', 'a'] // ++a
163-
['++', 'a', null] // a++
164-
['px', [,5]] // 5px (unit suffix)
96+
import justin from 'subscript/justin.js'
97+
justin('[1,2,3] ∩ [2,3,4]')({}) // [2, 3]
16598
```
16699

167-
### Stringify
100+
See [docs.md](./docs.md) for full API.
101+
102+
103+
## Syntax Tree
168104

169-
To convert tree back to code, there's codegenerator function:
105+
Expressions parse to a minimal JSON-compatible AST:
170106

171107
```js
172-
import { stringify } from 'subscript.js'
108+
import { parse } from 'subscript'
173109

174-
stringify(['+', ['*', 'min', [,60]], [,'sec']])
175-
// 'min * 60 + "sec"'
110+
parse('a + b * 2')
111+
// ['+', 'a', ['*', 'b', [, 2]]]
176112
```
177113

178-
## Extending
179-
180-
_Subscript_ provides premade language [features](./feature) and API to customize syntax:
114+
AST has simplified lispy tree structure (inspired by [frisk](https://ghub.io/frisk) / [nisp](https://github.com/ysmood/nisp)), opposed to [ESTree](https://github.com/estree/estree):
181115

182-
* `unary(str, precedence, postfix=false)` − register unary operator, either prefix `⚬a` or postfix `a⚬`.
183-
* `binary(str, precedence, rassoc=false)` − register binary operator `a ⚬ b`, optionally right-associative.
184-
* `nary(str, precedence)` − register n-ary (sequence) operator like `a; b;` or `a, b`, allows missing args.
185-
* `group(str, precedence)` - register group, like `[a]`, `{a}`, `(a)` etc.
186-
* `access(str, precedence)` - register access operator, like `a[b]`, `a(b)` etc.
187-
* `token(str, precedence, lnode => node)` − register custom token or literal. Callback takes left-side node and returns complete expression node.
188-
* `operator(str, (a, b) => ctx => value)` − register evaluator for an operator. Callback takes node arguments and returns evaluator function.
116+
* not limited to particular language (JS), can be compiled to different targets;
117+
* reflects execution sequence, rather than code layout;
118+
* has minimal overhead, directly maps to operators;
119+
* simplifies manual evaluation and debugging;
120+
* has conventional form and one-liner docs:
189121

190-
Longer operators should be registered after shorter ones, eg. first `|`, then `||`, then `||=`.
122+
Three forms:
191123

192124
```js
193-
import script, { compile, operator, unary, binary, token } from './subscript.js'
125+
'x' // identifier — resolve from context
126+
[, value] // literal — return as-is (empty slot = data)
127+
[op, ...args] // operation — apply operator
128+
```
194129

195-
// enable objects/arrays syntax
196-
import 'subscript/feature/array.js';
197-
import 'subscript/feature/object.js';
130+
See [spec.md](./spec.md).
198131

199-
// add identity operators (precedence of comparison)
200-
binary('===', 9), binary('!==', 9)
201-
operator('===', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)===b(ctx)))
202-
operator('!==', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)!==b(ctx)))
203132

204-
// add nullish coalescing (precedence of logical or)
205-
binary('??', 3)
206-
operator('??', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) ?? b(ctx)))
133+
## Safety
207134

208-
// add JS literals
209-
token('undefined', 20, a => a ? err() : [, undefined])
210-
token('NaN', 20, a => a ? err() : [, NaN])
135+
Blocked by default:
136+
- `__proto__`, `__defineGetter__`, `__defineSetter__`
137+
- `constructor`, `prototype`
138+
- Global access (only context is visible)
139+
140+
```js
141+
subscript('constructor.constructor("alert(1)")()')({})
142+
// undefined (blocked)
211143
```
212144

213-
See [`./feature/*`](./feature) or [`./justin.js`](./justin.js) for examples.
145+
## Performance
214146

147+
```
148+
Parse 30k: subscript 150ms · justin 183ms · jsep 270ms · expr-eval 480ms · jexl 1056ms
149+
Eval 30k: new Function 7ms · subscript 15ms · jsep+eval 30ms · expr-eval 72ms
150+
```
215151

152+
## Utils
216153

217-
## Performance
154+
### Codegen
218155

219-
Subscript shows good performance within other evaluators. Example expression:
156+
Convert tree back to code:
220157

221-
```
222-
1 + (a * b / c % d) - 2.0 + -3e-3 * +4.4e4 / f.g[0] - i.j(+k == 1)(0)
158+
```js
159+
import { codegen } from 'subscript/util/stringify.js'
160+
161+
codegen(['+', ['*', 'min', [,60]], [,'sec']])
162+
// 'min * 60 + "sec"'
223163
```
224164

225-
Parse 30k times:
165+
### Bundle
226166

227-
```
228-
subscript: ~150 ms 🥇
229-
justin: ~183 ms
230-
jsep: ~270 ms 🥈
231-
jexpr: ~297 ms 🥉
232-
mr-parser: ~420 ms
233-
expr-eval: ~480 ms
234-
math-parser: ~570 ms
235-
math-expression-evaluator: ~900ms
236-
jexl: ~1056 ms
237-
mathjs: ~1200 ms
238-
new Function: ~1154 ms
239-
```
167+
Create custom dialect as single file:
240168

241-
Eval 30k times:
242-
```
243-
new Function: ~7 ms 🥇
244-
subscript: ~15 ms 🥈
245-
justin: ~17 ms
246-
jexpr: ~23 ms 🥉
247-
jsep (expression-eval): ~30 ms
248-
math-expression-evaluator: ~50ms
249-
expr-eval: ~72 ms
250-
jexl: ~110 ms
251-
mathjs: ~119 ms
252-
mr-parser: -
253-
math-parser: -
169+
```js
170+
import { bundle } from 'subscript/util/bundle.js'
171+
172+
const code = await bundle('subscript/jessie.js')
173+
// → self-contained ES module
254174
```
255175

256-
<!--
176+
257177
## Used by
258178

259-
[prepr](https://github.com/dy/prepr), [justin](#justin), [jz](https://github.com/dy/jz), [glsl-transpiler](https://github.com/stackgl/glsl-transpiler), [piezo](https://github.com/dy/piezo)
260-
-->
179+
* [jz](https://github.com/dy/jz) — JS subset → WASM compiler
180+
<!-- * [prepr](https://github.com/dy/prepr) -->
181+
<!-- * [glsl-transpiler](https://github.com/stackgl/glsl-transpiler) -->
182+
<!-- * [piezo](https://github.com/dy/piezo) -->
183+
184+
185+
## Refs
261186

262-
## Alternatives
187+
[jsep](https://github.com/EricSmekens/jsep), [jexl](https://github.com/TomFrost/Jexl), [expr-eval](https://github.com/silentmatt/expr-eval), [math.js](https://mathjs.org/).
263188

264-
[jexpr](https://github.com/justinfagnani/jexpr), [jsep](https://github.com/EricSmekens/jsep), [jexl](https://github.com/TomFrost/Jexl), [mozjexl](https://github.com/mozilla/mozjexl), [expr-eval](https://github.com/silentmatt/expr-eval), [expression-eval](https://github.com/donmccurdy/expression-eval), [string-math](https://github.com/devrafalko/string-math), [nerdamer](https://github.com/jiggzson/nerdamer), [math-codegen](https://github.com/mauriciopoppe/math-codegen), [math-parser](https://www.npmjs.com/package/math-parser), [math.js](https://mathjs.org/docs/expressions/parsing.html), [nx-compile](https://github.com/nx-js/compiler-util), [built-in-math-eval](https://github.com/mauriciopoppe/built-in-math-eval)
189+
<!-- [mozjexl](https://github.com/mozilla/mozjexl), [jexpr](https://github.com/justinfagnani/jexpr), [expression-eval](https://github.com/donmccurdy/expression-eval), [string-math](https://github.com/devrafalko/string-math), [nerdamer](https://github.com/jiggzson/nerdamer), [math-codegen](https://github.com/mauriciopoppe/math-codegen), [math-parser](https://www.npmjs.com/package/math-parser), [nx-compile](https://github.com/nx-js/compiler-util), [built-in-math-eval](https://github.com/mauriciopoppe/built-in-math-eval) -->
265190

266-
<p align=center><a href="https://github.com/krsnzd/license/">🕉</a></p>
191+
<p align=center><a href="https://github.com/krsnzd/license/"></a></p>

0 commit comments

Comments
 (0)