subscript 9.2.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -169
- package/feature/access.js +67 -7
- package/feature/accessor.js +49 -0
- package/feature/asi.js +15 -0
- package/feature/async.js +45 -0
- package/feature/block.js +41 -0
- package/feature/class.js +69 -0
- package/feature/collection.js +40 -0
- package/feature/comment.js +25 -5
- package/feature/destruct.js +33 -0
- package/feature/function.js +44 -0
- package/feature/group.js +39 -9
- package/feature/if.js +23 -38
- package/feature/literal.js +13 -0
- package/feature/loop.js +107 -106
- package/feature/module.js +42 -0
- package/feature/number.js +41 -38
- package/feature/op/arithmetic.js +29 -0
- package/feature/op/arrow.js +33 -0
- package/feature/op/assign-logical.js +33 -0
- package/feature/op/assignment.js +47 -0
- package/feature/op/bitwise-unsigned.js +17 -0
- package/feature/op/bitwise.js +29 -0
- package/feature/op/comparison.js +19 -0
- package/feature/op/defer.js +15 -0
- package/feature/op/equality.js +16 -0
- package/feature/op/identity.js +15 -0
- package/feature/op/increment.js +23 -0
- package/feature/op/logical.js +21 -0
- package/feature/op/membership.js +17 -0
- package/feature/op/nullish.js +13 -0
- package/feature/op/optional.js +61 -0
- package/feature/op/pow.js +19 -0
- package/feature/op/range.js +26 -0
- package/feature/op/spread.js +15 -0
- package/feature/op/ternary.js +15 -0
- package/feature/op/type.js +18 -0
- package/feature/op/unary.js +41 -0
- package/feature/prop.js +34 -0
- package/feature/regex.js +31 -0
- package/feature/seq.js +21 -0
- package/feature/string.js +24 -17
- package/feature/switch.js +48 -0
- package/feature/template.js +39 -0
- package/feature/try.js +57 -0
- package/feature/unit.js +35 -0
- package/feature/var.js +51 -41
- package/jessie.js +31 -0
- package/jessie.min.js +8 -0
- package/justin.js +39 -48
- package/justin.min.js +8 -4
- package/package.json +15 -16
- package/parse.js +153 -0
- package/subscript.d.ts +45 -5
- package/subscript.js +62 -22
- package/subscript.min.js +5 -4
- package/util/bundle.js +507 -0
- package/util/stringify.js +172 -0
- package/feature/add.js +0 -22
- package/feature/array.js +0 -11
- package/feature/arrow.js +0 -23
- package/feature/assign.js +0 -11
- package/feature/bitwise.js +0 -11
- package/feature/bool.js +0 -5
- package/feature/call.js +0 -15
- package/feature/compare.js +0 -11
- package/feature/control.js +0 -142
- package/feature/increment.js +0 -11
- package/feature/logic.js +0 -11
- package/feature/mult.js +0 -25
- package/feature/object.js +0 -17
- package/feature/optional.js +0 -23
- package/feature/pow.js +0 -5
- package/feature/shift.js +0 -12
- package/feature/spread.js +0 -6
- package/feature/ternary.js +0 -10
- package/src/compile.d.ts +0 -17
- package/src/compile.js +0 -28
- package/src/const.js +0 -45
- package/src/parse.d.ts +0 -22
- package/src/parse.js +0 -113
- package/src/stringify.js +0 -27
- /package/{LICENSE → license} +0 -0
package/README.md
CHANGED
|
@@ -1,97 +1,67 @@
|
|
|
1
|
-
# sub<
|
|
1
|
+
# sub<sub><sup> ✦ </sup></sub>script [](https://github.com/dy/subscript/actions/workflows/node.js.yml) [](http://npmjs.org/subscript) [](https://bundlephobia.com/package/subscript) [](https://dy.github.io/subscript/) [](http://microjs.com/#subscript)
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Tiny expression parser & evaluator.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
* subsets of languages
|
|
9
|
-
* sandboxes, playgrounds, safe eval
|
|
10
|
-
* custom DSL
|
|
11
|
-
* preprocessors
|
|
12
|
-
* templates
|
|
11
|
+
## Usage
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
```js
|
|
14
|
+
import subscript from 'subscript'
|
|
15
15
|
|
|
16
|
+
let fn = subscript('a + b * 2')
|
|
17
|
+
fn({ a: 1, b: 3 }) // 7
|
|
18
|
+
```
|
|
16
19
|
|
|
17
|
-
##
|
|
20
|
+
## Presets
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
import subscript from './subscript.js'
|
|
22
|
+
### subscript
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
const fn = subscript('a.b + Math.sqrt(c - 1)')
|
|
24
|
+
Common expressions:
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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:
|
|
64
36
|
|
|
37
|
+
`` 'str' 0x 0b === !== ** ?? >>> ?. ? : => ... [] {} ` // /**/ true false null ``
|
|
65
38
|
```js
|
|
66
|
-
import justin from 'subscript/justin'
|
|
39
|
+
import justin from 'subscript/justin.js'
|
|
67
40
|
|
|
68
|
-
|
|
69
|
-
|
|
41
|
+
justin('{ x: a?.b ?? 0, y: [1, ...rest] }')({ a: null, rest: [2, 3] })
|
|
42
|
+
// { x: 0, y: [1, 2, 3] }
|
|
70
43
|
```
|
|
71
44
|
|
|
72
|
-
###
|
|
73
|
-
|
|
74
|
-
For statement syntax, import `subscript/feature/control.js`:
|
|
45
|
+
### jessie
|
|
75
46
|
|
|
76
|
-
+
|
|
77
|
-
+ `while (c) body`
|
|
78
|
-
+ `for (init; cond; step) body`
|
|
79
|
-
+ `{ a; b }` — block scope
|
|
80
|
-
+ `let x`, `const x = 1`
|
|
81
|
-
+ `break`, `continue`, `return x`
|
|
47
|
+
JSON + expressions + statements, functions:
|
|
82
48
|
|
|
49
|
+
`if else for while do let const var function class return throw try catch switch import export /regex/`
|
|
83
50
|
```js
|
|
84
|
-
import
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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)
|
|
91
59
|
`)
|
|
92
|
-
|
|
60
|
+
fn({}) // 120
|
|
93
61
|
```
|
|
94
62
|
|
|
63
|
+
Jessie can parse and compile its own source.
|
|
64
|
+
|
|
95
65
|
|
|
96
66
|
## Parse / Compile
|
|
97
67
|
|
|
@@ -109,137 +79,113 @@ fn = compile(tree)
|
|
|
109
79
|
fn({ a: {b: 1}, c: 2 }) // 2
|
|
110
80
|
```
|
|
111
81
|
|
|
112
|
-
|
|
82
|
+
## Extension
|
|
113
83
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
```
|
|
121
94
|
|
|
122
95
|
```js
|
|
123
|
-
import
|
|
124
|
-
|
|
125
|
-
const fn = compile(['+', ['*', 'min', [,60]], [,'sec']])
|
|
126
|
-
fn({min: 5}) // min*60 + "sec" == "300sec"
|
|
127
|
-
|
|
128
|
-
// node kinds
|
|
129
|
-
['+', a]; // unary operator `+a`
|
|
130
|
-
['+', a, b]; // binary operator `a + b`
|
|
131
|
-
['+', a, b, c]; // n-ary operator `a + b + c`
|
|
132
|
-
['()', a]; // group operator `(a)`
|
|
133
|
-
['()', a, b]; // access operator `a(b)`
|
|
134
|
-
[, 'a']; // literal value `'a'`
|
|
135
|
-
'a'; // variable (from scope)
|
|
136
|
-
null|empty; // placeholder
|
|
137
|
-
|
|
138
|
-
// eg.
|
|
139
|
-
['()', 'a'] // (a)
|
|
140
|
-
['()', 'a', null] // a()
|
|
141
|
-
['()', 'a', 'b'] // a(b)
|
|
142
|
-
['++', 'a'] // ++a
|
|
143
|
-
['++','a', null] // a++
|
|
96
|
+
import justin from 'subscript/justin.js'
|
|
97
|
+
justin('[1,2,3] ∩ [2,3,4]')({}) // [2, 3]
|
|
144
98
|
```
|
|
145
99
|
|
|
146
|
-
|
|
100
|
+
See [docs.md](./docs.md) for full API.
|
|
101
|
+
|
|
147
102
|
|
|
148
|
-
|
|
103
|
+
## Syntax Tree
|
|
104
|
+
|
|
105
|
+
Expressions parse to a minimal JSON-compatible AST:
|
|
149
106
|
|
|
150
107
|
```js
|
|
151
|
-
import {
|
|
108
|
+
import { parse } from 'subscript'
|
|
152
109
|
|
|
153
|
-
|
|
154
|
-
// '
|
|
110
|
+
parse('a + b * 2')
|
|
111
|
+
// ['+', 'a', ['*', 'b', [, 2]]]
|
|
155
112
|
```
|
|
156
113
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
_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):
|
|
160
115
|
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* `token(str, precedence, lnode => node)` − register custom token or literal. Callback takes left-side node and returns complete expression node.
|
|
167
|
-
* `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:
|
|
168
121
|
|
|
169
|
-
|
|
122
|
+
Three forms:
|
|
170
123
|
|
|
171
124
|
```js
|
|
172
|
-
|
|
125
|
+
'x' // identifier — resolve from context
|
|
126
|
+
[, value] // literal — return as-is (empty slot = data)
|
|
127
|
+
[op, ...args] // operation — apply operator
|
|
128
|
+
```
|
|
173
129
|
|
|
174
|
-
|
|
175
|
-
import 'subscript/feature/array.js';
|
|
176
|
-
import 'subscript/feature/object.js';
|
|
130
|
+
See [spec.md](./spec.md).
|
|
177
131
|
|
|
178
|
-
// add identity operators (precedence of comparison)
|
|
179
|
-
binary('===', 9), binary('!==', 9)
|
|
180
|
-
operator('===', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)===b(ctx)))
|
|
181
|
-
operator('!==', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)!==b(ctx)))
|
|
182
132
|
|
|
183
|
-
|
|
184
|
-
binary('??', 3)
|
|
185
|
-
operator('??', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) ?? b(ctx)))
|
|
133
|
+
## Safety
|
|
186
134
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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)
|
|
190
143
|
```
|
|
191
144
|
|
|
192
|
-
|
|
145
|
+
## Performance
|
|
193
146
|
|
|
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
|
+
```
|
|
194
151
|
|
|
152
|
+
## Utils
|
|
195
153
|
|
|
196
|
-
|
|
154
|
+
### Codegen
|
|
197
155
|
|
|
198
|
-
|
|
156
|
+
Convert tree back to code:
|
|
199
157
|
|
|
200
|
-
```
|
|
201
|
-
|
|
158
|
+
```js
|
|
159
|
+
import { codegen } from 'subscript/util/stringify.js'
|
|
160
|
+
|
|
161
|
+
codegen(['+', ['*', 'min', [,60]], [,'sec']])
|
|
162
|
+
// 'min * 60 + "sec"'
|
|
202
163
|
```
|
|
203
164
|
|
|
204
|
-
|
|
165
|
+
### Bundle
|
|
205
166
|
|
|
206
|
-
|
|
207
|
-
subscript: ~150 ms 🥇
|
|
208
|
-
justin: ~183 ms
|
|
209
|
-
jsep: ~270 ms 🥈
|
|
210
|
-
jexpr: ~297 ms 🥉
|
|
211
|
-
mr-parser: ~420 ms
|
|
212
|
-
expr-eval: ~480 ms
|
|
213
|
-
math-parser: ~570 ms
|
|
214
|
-
math-expression-evaluator: ~900ms
|
|
215
|
-
jexl: ~1056 ms
|
|
216
|
-
mathjs: ~1200 ms
|
|
217
|
-
new Function: ~1154 ms
|
|
218
|
-
```
|
|
167
|
+
Create custom dialect as single file:
|
|
219
168
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
jexpr: ~23 ms 🥉
|
|
226
|
-
jsep (expression-eval): ~30 ms
|
|
227
|
-
math-expression-evaluator: ~50ms
|
|
228
|
-
expr-eval: ~72 ms
|
|
229
|
-
jexl: ~110 ms
|
|
230
|
-
mathjs: ~119 ms
|
|
231
|
-
mr-parser: -
|
|
232
|
-
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
|
|
233
174
|
```
|
|
234
175
|
|
|
235
|
-
|
|
176
|
+
|
|
236
177
|
## Used by
|
|
237
178
|
|
|
238
|
-
|
|
239
|
-
-->
|
|
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
|
|
240
186
|
|
|
241
|
-
|
|
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/).
|
|
242
188
|
|
|
243
|
-
|
|
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) -->
|
|
244
190
|
|
|
245
|
-
<p align=center><a href="https://github.com/krsnzd/license/"
|
|
191
|
+
<p align=center><a href="https://github.com/krsnzd/license/">ॐ</a></p>
|
package/feature/access.js
CHANGED
|
@@ -1,11 +1,71 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Property access: a.b, a[b], a(b), [1,2,3]
|
|
3
|
+
* For private fields (#x), see class.js
|
|
4
|
+
*/
|
|
5
|
+
import { access, binary, operator, compile } from '../parse.js';
|
|
6
|
+
|
|
7
|
+
// Block prototype chain attacks
|
|
8
|
+
export const unsafe = k => k?.[0] === '_' && k[1] === '_' || k === 'constructor' || k === 'prototype';
|
|
9
|
+
|
|
10
|
+
const ACCESS = 170;
|
|
4
11
|
|
|
5
12
|
// a[b]
|
|
6
|
-
access('[]',
|
|
7
|
-
operator('[]', (a, b) => !b ? err() : (a = compile(a), b = compile(b), ctx => { const k = b(ctx); return unsafe(k) ? undefined : a(ctx)[k] }))
|
|
13
|
+
access('[]', ACCESS);
|
|
8
14
|
|
|
9
15
|
// a.b
|
|
10
|
-
binary('.',
|
|
11
|
-
|
|
16
|
+
binary('.', ACCESS);
|
|
17
|
+
|
|
18
|
+
// a(b,c,d), a()
|
|
19
|
+
access('()', ACCESS);
|
|
20
|
+
|
|
21
|
+
// Compile
|
|
22
|
+
const err = msg => { throw Error(msg) };
|
|
23
|
+
operator('[]', (a, b) => {
|
|
24
|
+
// Array literal: [1,2,3] - b is strictly undefined (AST length 2)
|
|
25
|
+
if (b === undefined) {
|
|
26
|
+
a = !a ? [] : a[0] === ',' ? a.slice(1) : [a];
|
|
27
|
+
a = a.map(a => a == null ? (() => undefined) : a[0] === '...' ? (a = compile(a[1]), ctx => a(ctx)) : (a = compile(a), ctx => [a(ctx)]));
|
|
28
|
+
return ctx => a.flatMap(a => a(ctx));
|
|
29
|
+
}
|
|
30
|
+
// Member access: a[b]
|
|
31
|
+
if (b == null) err('Missing index');
|
|
32
|
+
a = compile(a); b = compile(b);
|
|
33
|
+
return ctx => { const k = b(ctx); return unsafe(k) ? undefined : a(ctx)[k]; };
|
|
34
|
+
});
|
|
35
|
+
operator('.', (a, b) => (a = compile(a), b = !b[0] ? b[1] : b, unsafe(b) ? () => undefined : ctx => a(ctx)[b]));
|
|
36
|
+
operator('()', (a, b) => {
|
|
37
|
+
// Group: (expr) - no second argument means grouping, not call
|
|
38
|
+
if (b === undefined) return a == null ? err('Empty ()') : compile(a);
|
|
39
|
+
// Validate: no sparse arguments in calls
|
|
40
|
+
const hasSparse = n => n?.[0] === ',' && n.slice(1).some(a => a == null || hasSparse(a));
|
|
41
|
+
if (hasSparse(b)) err('Empty argument');
|
|
42
|
+
const args = !b ? () => [] :
|
|
43
|
+
b[0] === ',' ? (b = b.slice(1).map(compile), ctx => b.map(arg => arg(ctx))) :
|
|
44
|
+
(b = compile(b), ctx => [b(ctx)]);
|
|
45
|
+
return prop(a, (obj, path, ctx) => obj[path](...args(ctx)), true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Left-value check (valid assignment target)
|
|
49
|
+
export const isLval = n =>
|
|
50
|
+
typeof n === 'string' ||
|
|
51
|
+
(Array.isArray(n) && (
|
|
52
|
+
n[0] === '.' || n[0] === '?.' ||
|
|
53
|
+
(n[0] === '[]' && n.length === 3) || n[0] === '?.[]' ||
|
|
54
|
+
(n[0] === '()' && n.length === 2 && isLval(n[1])) ||
|
|
55
|
+
n[0] === '{}'
|
|
56
|
+
));
|
|
57
|
+
|
|
58
|
+
// Compile error helper
|
|
59
|
+
const compileErr = msg => { throw Error(msg) };
|
|
60
|
+
|
|
61
|
+
// Property accessor helper - compiles property access pattern to evaluator
|
|
62
|
+
export const prop = (a, fn, generic, obj, path) => (
|
|
63
|
+
a == null ? compileErr('Empty ()') :
|
|
64
|
+
a[0] === '()' && a.length == 2 ? prop(a[1], fn, generic) :
|
|
65
|
+
typeof a === 'string' ? ctx => fn(ctx, a, ctx) :
|
|
66
|
+
a[0] === '.' ? (obj = compile(a[1]), path = a[2], ctx => fn(obj(ctx), path, ctx)) :
|
|
67
|
+
a[0] === '?.' ? (obj = compile(a[1]), path = a[2], ctx => { const o = obj(ctx); return o == null ? undefined : fn(o, path, ctx); }) :
|
|
68
|
+
a[0] === '[]' && a.length === 3 ? (obj = compile(a[1]), path = compile(a[2]), ctx => fn(obj(ctx), path(ctx), ctx)) :
|
|
69
|
+
a[0] === '?.[]' ? (obj = compile(a[1]), path = compile(a[2]), ctx => { const o = obj(ctx); return o == null ? undefined : fn(o, path(ctx), ctx); }) :
|
|
70
|
+
(a = compile(a), ctx => fn([a(ctx)], 0, ctx))
|
|
71
|
+
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object accessor properties (getters/setters)
|
|
3
|
+
*
|
|
4
|
+
* AST:
|
|
5
|
+
* { get x() { body } } → ['{}', ['get', 'x', body]]
|
|
6
|
+
* { set x(v) { body } } → ['{}', ['set', 'x', 'v', body]]
|
|
7
|
+
*/
|
|
8
|
+
import { token, expr, skip, space, err, next, parse, cur, idx, operator, compile } from '../parse.js';
|
|
9
|
+
|
|
10
|
+
// Accessor marker for object property definitions
|
|
11
|
+
export const ACC = Symbol('accessor');
|
|
12
|
+
|
|
13
|
+
const ASSIGN = 20;
|
|
14
|
+
const OPAREN = 40, CPAREN = 41, OBRACE = 123, CBRACE = 125;
|
|
15
|
+
|
|
16
|
+
// Shared parser for get/set — returns false if not valid accessor pattern (falls through to identifier)
|
|
17
|
+
// Returns false (not undefined) to signal "fall through without setting reserved"
|
|
18
|
+
const accessor = (kind) => a => {
|
|
19
|
+
if (a) return; // not prefix
|
|
20
|
+
space();
|
|
21
|
+
const name = next(parse.id);
|
|
22
|
+
if (!name) return false; // no property name = not accessor (e.g. `{ get: 1 }`)
|
|
23
|
+
space();
|
|
24
|
+
if (cur.charCodeAt(idx) !== OPAREN) return false; // not followed by ( = not accessor
|
|
25
|
+
skip();
|
|
26
|
+
const params = expr(0, CPAREN);
|
|
27
|
+
space();
|
|
28
|
+
if (cur.charCodeAt(idx) !== OBRACE) return false;
|
|
29
|
+
skip();
|
|
30
|
+
return [kind, name, params, expr(0, CBRACE)];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
token('get', ASSIGN - 1, accessor('get'));
|
|
34
|
+
token('set', ASSIGN - 1, accessor('set'));
|
|
35
|
+
|
|
36
|
+
// Compile
|
|
37
|
+
operator('get', (name, body) => {
|
|
38
|
+
body = body ? compile(body) : () => {};
|
|
39
|
+
return ctx => [[ACC, name, {
|
|
40
|
+
get: function() { const s = Object.create(ctx || {}); s.this = this; return body(s); }
|
|
41
|
+
}]];
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
operator('set', (name, param, body) => {
|
|
45
|
+
body = body ? compile(body) : () => {};
|
|
46
|
+
return ctx => [[ACC, name, {
|
|
47
|
+
set: function(v) { const s = Object.create(ctx || {}); s.this = this; s[param] = v; body(s); }
|
|
48
|
+
}]];
|
|
49
|
+
});
|
package/feature/asi.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic Semicolon Insertion (ASI)
|
|
3
|
+
*
|
|
4
|
+
* JS-style ASI: insert virtual ; when newline precedes illegal token at statement level
|
|
5
|
+
*/
|
|
6
|
+
import { parse } from '../parse.js';
|
|
7
|
+
|
|
8
|
+
const STATEMENT = 5;
|
|
9
|
+
|
|
10
|
+
parse.asi = (token, prec, expr) => {
|
|
11
|
+
if (prec >= STATEMENT) return; // only at statement level
|
|
12
|
+
const next = expr(STATEMENT - .5);
|
|
13
|
+
if (!next) return;
|
|
14
|
+
return token?.[0] !== ';' ? [';', token, next] : (token.push(next), token);
|
|
15
|
+
};
|
package/feature/async.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Async/await/yield: async function, async arrow, await, yield expressions
|
|
2
|
+
import { unary, expr, skip, space, cur, idx, word, operator, compile } from '../parse.js';
|
|
3
|
+
import { keyword } from './block.js';
|
|
4
|
+
|
|
5
|
+
const PREFIX = 140, ASSIGN = 20;
|
|
6
|
+
|
|
7
|
+
// await expr → ['await', expr]
|
|
8
|
+
unary('await', PREFIX);
|
|
9
|
+
|
|
10
|
+
// yield expr → ['yield', expr]
|
|
11
|
+
// yield* expr → ['yield*', expr]
|
|
12
|
+
keyword('yield', PREFIX, () => {
|
|
13
|
+
space();
|
|
14
|
+
if (cur[idx] === '*') {
|
|
15
|
+
skip();
|
|
16
|
+
space();
|
|
17
|
+
return ['yield*', expr(ASSIGN)];
|
|
18
|
+
}
|
|
19
|
+
return ['yield', expr(ASSIGN)];
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// async function name() {} → ['async', ['function', name, params, body]]
|
|
23
|
+
// async () => {} → ['async', ['=>', params, body]]
|
|
24
|
+
// async x => {} → ['async', ['=>', x, body]]
|
|
25
|
+
keyword('async', PREFIX, () => {
|
|
26
|
+
space();
|
|
27
|
+
// async function - check for 'function' word
|
|
28
|
+
if (word('function')) return ['async', expr(PREFIX)];
|
|
29
|
+
// async arrow: async () => or async x =>
|
|
30
|
+
// Parse at assign precedence to catch => operator
|
|
31
|
+
const params = expr(ASSIGN - .5);
|
|
32
|
+
return params && ['async', params];
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Compile
|
|
36
|
+
operator('async', fn => {
|
|
37
|
+
const inner = compile(fn);
|
|
38
|
+
return ctx => {
|
|
39
|
+
const f = inner(ctx);
|
|
40
|
+
return async function(...args) { return f(...args); };
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
operator('await', a => (a = compile(a), async ctx => await a(ctx)));
|
|
44
|
+
operator('yield', a => (a = a ? compile(a) : null, ctx => { throw { __yield__: a ? a(ctx) : undefined }; }));
|
|
45
|
+
operator('yield*', a => (a = compile(a), ctx => { throw { __yield_all__: a(ctx) }; }));
|
package/feature/block.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Block parsing helpers
|
|
2
|
+
import { expr, skip, space, lookup, err, parse, seek, cur, idx, parens, loc, operator, compile } from '../parse.js';
|
|
3
|
+
|
|
4
|
+
const STATEMENT = 5, OBRACE = 123, CBRACE = 125;
|
|
5
|
+
|
|
6
|
+
// keyword(op, prec, fn) - prefix-only word token
|
|
7
|
+
// keyword('while', 6, () => ['while', parens(), body()])
|
|
8
|
+
// keyword('break', 6, () => ['break'])
|
|
9
|
+
// attaches .loc to array results for source mapping
|
|
10
|
+
export const keyword = (op, prec, map, c = op.charCodeAt(0), l = op.length, prev = lookup[c], r) =>
|
|
11
|
+
lookup[c] = (a, curPrec, curOp, from = idx) =>
|
|
12
|
+
!a &&
|
|
13
|
+
(curOp ? op == curOp : (l < 2 || cur.substr(idx, l) == op) && (curOp = op)) &&
|
|
14
|
+
curPrec < prec &&
|
|
15
|
+
!parse.id(cur.charCodeAt(idx + l)) &&
|
|
16
|
+
(seek(idx + l), (r = map()) ? loc(r, from) : (seek(from), !prev && err()), r) ||
|
|
17
|
+
prev?.(a, curPrec, curOp);
|
|
18
|
+
|
|
19
|
+
// infix(op, prec, fn) - infix word token (requires left operand)
|
|
20
|
+
// infix('catch', 6, a => ['catch', a, parens(), block()])
|
|
21
|
+
// infix('finally', 6, a => ['finally', a, block()])
|
|
22
|
+
// attaches .loc to array results for source mapping
|
|
23
|
+
export const infix = (op, prec, map, c = op.charCodeAt(0), l = op.length, prev = lookup[c], r) =>
|
|
24
|
+
lookup[c] = (a, curPrec, curOp, from = idx) =>
|
|
25
|
+
a &&
|
|
26
|
+
(curOp ? op == curOp : (l < 2 || cur.substr(idx, l) == op) && (curOp = op)) &&
|
|
27
|
+
curPrec < prec &&
|
|
28
|
+
!parse.id(cur.charCodeAt(idx + l)) &&
|
|
29
|
+
(seek(idx + l), loc(r = map(a), from), r) ||
|
|
30
|
+
prev?.(a, curPrec, curOp);
|
|
31
|
+
|
|
32
|
+
// block() - parse required { body }
|
|
33
|
+
export const block = () =>
|
|
34
|
+
(space() === OBRACE || err('Expected {'), skip(), expr(STATEMENT - .5, CBRACE) || null);
|
|
35
|
+
|
|
36
|
+
// body() - parse { body } or single statement
|
|
37
|
+
export const body = () =>
|
|
38
|
+
space() !== OBRACE ? expr(STATEMENT + .5) : (skip(), ['block', expr(STATEMENT - .5, CBRACE) || null]);
|
|
39
|
+
|
|
40
|
+
// Compile
|
|
41
|
+
operator('block', body => body === undefined ? () => {} : (body = compile(body), ctx => body(ctx)));
|
package/feature/class.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Class declarations and expressions
|
|
2
|
+
// class A extends B { ... }
|
|
3
|
+
import { binary, unary, token, expr, space, next, parse, literal, word, operator, compile } from '../parse.js';
|
|
4
|
+
import { keyword, block } from './block.js';
|
|
5
|
+
|
|
6
|
+
const TOKEN = 200, PREFIX = 140, COMP = 90;
|
|
7
|
+
const STATIC = Symbol('static');
|
|
8
|
+
|
|
9
|
+
// super → literal
|
|
10
|
+
literal('super', Symbol.for('super'));
|
|
11
|
+
|
|
12
|
+
// static member → ['static', member]
|
|
13
|
+
unary('static', PREFIX);
|
|
14
|
+
|
|
15
|
+
// instanceof: object instanceof Constructor
|
|
16
|
+
binary('instanceof', COMP);
|
|
17
|
+
|
|
18
|
+
// #private fields: #x → '#x' (identifier starting with #)
|
|
19
|
+
token('#', TOKEN, a => {
|
|
20
|
+
if (a) return;
|
|
21
|
+
const id = next(parse.id);
|
|
22
|
+
return id ? '#' + id : void 0;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// class [Name] [extends Base] { body }
|
|
26
|
+
keyword('class', TOKEN, () => {
|
|
27
|
+
space();
|
|
28
|
+
let name = next(parse.id) || null;
|
|
29
|
+
// 'extends' parsed as name? → anonymous class
|
|
30
|
+
if (name === 'extends') name = null;
|
|
31
|
+
else {
|
|
32
|
+
space();
|
|
33
|
+
if (!word('extends')) return ['class', name, null, block()];
|
|
34
|
+
}
|
|
35
|
+
space();
|
|
36
|
+
return ['class', name, expr(TOKEN), block()];
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Compile
|
|
40
|
+
const err = msg => { throw Error(msg) };
|
|
41
|
+
operator('instanceof', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) instanceof b(ctx)));
|
|
42
|
+
operator('class', (name, base, body) => {
|
|
43
|
+
base = base ? compile(base) : null;
|
|
44
|
+
body = body ? compile(body) : null;
|
|
45
|
+
return ctx => {
|
|
46
|
+
const Parent = base ? base(ctx) : Object;
|
|
47
|
+
const cls = function(...args) {
|
|
48
|
+
if (!(this instanceof cls)) return err('Class constructor must be called with new');
|
|
49
|
+
const instance = base ? Reflect.construct(Parent, args, cls) : this;
|
|
50
|
+
if (cls.prototype.__constructor__) cls.prototype.__constructor__.apply(instance, args);
|
|
51
|
+
return instance;
|
|
52
|
+
};
|
|
53
|
+
Object.setPrototypeOf(cls.prototype, Parent.prototype);
|
|
54
|
+
Object.setPrototypeOf(cls, Parent);
|
|
55
|
+
if (body) {
|
|
56
|
+
const methods = Object.create(ctx);
|
|
57
|
+
methods['super'] = Parent;
|
|
58
|
+
const entries = body(methods);
|
|
59
|
+
const items = Array.isArray(entries) && typeof entries[0]?.[0] === 'string' ? entries : [];
|
|
60
|
+
for (const [k, v] of items) {
|
|
61
|
+
if (k === 'constructor') cls.prototype.__constructor__ = v;
|
|
62
|
+
else cls.prototype[k] = v;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (name) ctx[name] = cls;
|
|
66
|
+
return cls;
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
operator('static', a => (a = compile(a), ctx => [[STATIC, a(ctx)]]));
|