subscript 9.1.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 +123 -171
- 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 -6
- package/feature/destruct.js +33 -0
- package/feature/function.js +44 -0
- package/feature/group.js +41 -12
- package/feature/if.js +28 -0
- package/feature/literal.js +13 -0
- package/feature/loop.js +123 -0
- package/feature/module.js +42 -0
- package/feature/number.js +45 -10
- 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 -16
- 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 +59 -0
- package/jessie.js +31 -0
- package/jessie.min.js +8 -0
- package/justin.js +39 -48
- package/justin.min.js +8 -1
- package/package.json +18 -23
- package/parse.js +153 -0
- package/subscript.d.ts +45 -5
- package/subscript.js +62 -22
- package/subscript.min.js +5 -1
- 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/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 -30
- 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 -42
- package/src/parse.d.ts +0 -22
- package/src/parse.js +0 -114
- package/src/stringify.js +0 -31
- /package/{LICENSE → license} +0 -0
package/README.md
CHANGED
|
@@ -1,75 +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 (eg. [justin](#justin)<!-- [jz](https://github.com/dy/jz) -->)
|
|
9
|
-
* sandboxes, playgrounds, safe eval (eg. [glsl-transpiler](https://github.com/stackgl/glsl-transpiler))
|
|
10
|
-
* custom DSL (eg. [piezo](https://github.com/dy/piezo)) <!-- uneural -->
|
|
11
|
-
* preprocessors (eg. [prepr](https://github.com/dy/prepr))
|
|
12
|
-
* templates (eg. [sprae](https://github.com/dy/sprae))
|
|
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`
|
|
64
|
-
<!-- + strings interpolation -->
|
|
33
|
+
### justin
|
|
34
|
+
|
|
35
|
+
JSON + expressions + templates + arrows:
|
|
65
36
|
|
|
37
|
+
`` 'str' 0x 0b === !== ** ?? >>> ?. ? : => ... [] {} ` // /**/ true false null ``
|
|
66
38
|
```js
|
|
67
|
-
import
|
|
39
|
+
import justin from 'subscript/justin.js'
|
|
68
40
|
|
|
69
|
-
|
|
70
|
-
|
|
41
|
+
justin('{ x: a?.b ?? 0, y: [1, ...rest] }')({ a: null, rest: [2, 3] })
|
|
42
|
+
// { x: 0, y: [1, 2, 3] }
|
|
71
43
|
```
|
|
72
44
|
|
|
45
|
+
### jessie
|
|
46
|
+
|
|
47
|
+
JSON + expressions + statements, functions:
|
|
48
|
+
|
|
49
|
+
`if else for while do let const var function class return throw try catch switch import export /regex/`
|
|
50
|
+
```js
|
|
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)
|
|
59
|
+
`)
|
|
60
|
+
fn({}) // 120
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Jessie can parse and compile its own source.
|
|
64
|
+
|
|
73
65
|
|
|
74
66
|
## Parse / Compile
|
|
75
67
|
|
|
@@ -87,7 +79,37 @@ fn = compile(tree)
|
|
|
87
79
|
fn({ a: {b: 1}, c: 2 }) // 2
|
|
88
80
|
```
|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
## Extension
|
|
83
|
+
|
|
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
|
+
```
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import justin from 'subscript/justin.js'
|
|
97
|
+
justin('[1,2,3] ∩ [2,3,4]')({}) // [2, 3]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
See [docs.md](./docs.md) for full API.
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## Syntax Tree
|
|
104
|
+
|
|
105
|
+
Expressions parse to a minimal JSON-compatible AST:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
import { parse } from 'subscript'
|
|
109
|
+
|
|
110
|
+
parse('a + b * 2')
|
|
111
|
+
// ['+', 'a', ['*', 'b', [, 2]]]
|
|
112
|
+
```
|
|
91
113
|
|
|
92
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):
|
|
93
115
|
|
|
@@ -97,143 +119,73 @@ AST has simplified lispy tree structure (inspired by [frisk](https://ghub.io/fri
|
|
|
97
119
|
* simplifies manual evaluation and debugging;
|
|
98
120
|
* has conventional form and one-liner docs:
|
|
99
121
|
|
|
122
|
+
Three forms:
|
|
123
|
+
|
|
100
124
|
```js
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
fn({min: 5}) // min*60 + "sec" == "300sec"
|
|
105
|
-
|
|
106
|
-
// node kinds
|
|
107
|
-
['+', a]; // unary operator `+a`
|
|
108
|
-
['+', a, b]; // binary operator `a + b`
|
|
109
|
-
['+', a, b, c]; // n-ary operator `a + b + c`
|
|
110
|
-
['()', a]; // group operator `(a)`
|
|
111
|
-
['()', a, b]; // access operator `a(b)`
|
|
112
|
-
[, 'a']; // literal value `'a'`
|
|
113
|
-
a; // variable (from scope)
|
|
114
|
-
null|empty; // placeholder
|
|
115
|
-
|
|
116
|
-
// eg.
|
|
117
|
-
['()', 'a'] // (a)
|
|
118
|
-
['()', 'a', null] // a()
|
|
119
|
-
['()', 'a', 'b'] // a(b)
|
|
120
|
-
['++', 'a'] // ++a
|
|
121
|
-
['++','a', null] // a++
|
|
125
|
+
'x' // identifier — resolve from context
|
|
126
|
+
[, value] // literal — return as-is (empty slot = data)
|
|
127
|
+
[op, ...args] // operation — apply operator
|
|
122
128
|
```
|
|
123
129
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
To convert tree back to code, there's codegenerator function:
|
|
130
|
+
See [spec.md](./spec.md).
|
|
127
131
|
|
|
128
|
-
```js
|
|
129
|
-
import { stringify } from 'subscript.js'
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
// 'min*60 + "sec" == "300sec"'
|
|
133
|
-
```
|
|
133
|
+
## Safety
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
Blocked by default:
|
|
136
|
+
- `__proto__`, `__defineGetter__`, `__defineSetter__`
|
|
137
|
+
- `constructor`, `prototype`
|
|
138
|
+
- Global access (only context is visible)
|
|
136
139
|
|
|
137
|
-
|
|
140
|
+
```js
|
|
141
|
+
subscript('constructor.constructor("alert(1)")()')({})
|
|
142
|
+
// undefined (blocked)
|
|
143
|
+
```
|
|
138
144
|
|
|
139
|
-
|
|
140
|
-
* `binary(str, precedence, rassoc=false)` − register binary operator `a ⚬ b`, optionally right-associative.
|
|
141
|
-
* `nary(str, precedence)` − register n-ary (sequence) operator like `a; b;` or `a, b`, allows missing args.
|
|
142
|
-
* `group(str, precedence)` - register group, like `[a]`, `{a}`, `(a)` etc.
|
|
143
|
-
* `access(str, precedence)` - register access operator, like `a[b]`, `a(b)` etc.
|
|
144
|
-
* `token(str, precedence, lnode => node)` − register custom token or literal. Callback takes left-side node and returns complete expression node.
|
|
145
|
-
* `operator(str, (a, b) => ctx => value)` − register evaluator for an operator. Callback takes node arguments and returns evaluator function.
|
|
145
|
+
## Performance
|
|
146
146
|
|
|
147
|
-
|
|
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
|
+
```
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
import script, { compile, operator, unary, binary, token } from './subscript.js'
|
|
152
|
+
## Utils
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
import 'subscript/feature/array.js';
|
|
154
|
-
import 'subscript/feature/object.js';
|
|
154
|
+
### Codegen
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
binary('===', 9), binary('!==', 9)
|
|
158
|
-
operator('===', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)===b(ctx)))
|
|
159
|
-
operator('===', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)!==b(ctx)))
|
|
156
|
+
Convert tree back to code:
|
|
160
157
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
operator('??', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) ?? b(ctx)))
|
|
158
|
+
```js
|
|
159
|
+
import { codegen } from 'subscript/util/stringify.js'
|
|
164
160
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
token('NaN', 20, a => a ? err() : [, NaN])
|
|
161
|
+
codegen(['+', ['*', 'min', [,60]], [,'sec']])
|
|
162
|
+
// 'min * 60 + "sec"'
|
|
168
163
|
```
|
|
169
164
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<!--
|
|
174
|
-
## Ideas
|
|
175
|
-
|
|
176
|
-
* Keyed arrays <code>[a:1, b:2, c:3]</code>
|
|
177
|
-
* 7!` (factorial)
|
|
178
|
-
* `5s`, `5rem` (units)
|
|
179
|
-
* `arrᵀ` - transpose
|
|
180
|
-
* `int 5` (typecast)
|
|
181
|
-
* `$a` (parameter expansion)
|
|
182
|
-
* `1 to 10 by 2`
|
|
183
|
-
* `a if b else c`
|
|
184
|
-
* `a, b in c`
|
|
185
|
-
* `a.xyz` swizzles
|
|
186
|
-
* vector operators
|
|
187
|
-
* set operators
|
|
188
|
-
* polynomial operators
|
|
189
|
-
* versions
|
|
190
|
-
* hashes, urls
|
|
191
|
-
* regexes
|
|
192
|
-
* 2a as `2*a`
|
|
193
|
-
* string interpolation ` ${} 1 ${} `
|
|
194
|
-
-->
|
|
165
|
+
### Bundle
|
|
195
166
|
|
|
196
|
-
|
|
167
|
+
Create custom dialect as single file:
|
|
197
168
|
|
|
198
|
-
|
|
169
|
+
```js
|
|
170
|
+
import { bundle } from 'subscript/util/bundle.js'
|
|
199
171
|
|
|
200
|
-
|
|
201
|
-
|
|
172
|
+
const code = await bundle('subscript/jessie.js')
|
|
173
|
+
// → self-contained ES module
|
|
202
174
|
```
|
|
203
175
|
|
|
204
|
-
Parse 30k times:
|
|
205
176
|
|
|
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
|
-
```
|
|
177
|
+
## Used by
|
|
219
178
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
math-expression-evaluator: ~50ms
|
|
228
|
-
expr-eval: ~72 ms
|
|
229
|
-
jexl: ~110 ms
|
|
230
|
-
mathjs: ~119 ms
|
|
231
|
-
mr-parser: -
|
|
232
|
-
math-parser: -
|
|
233
|
-
```
|
|
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
|
|
234
186
|
|
|
235
|
-
|
|
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/).
|
|
236
188
|
|
|
237
|
-
|
|
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) -->
|
|
238
190
|
|
|
239
|
-
<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 => a(ctx)[b(ctx)]))
|
|
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)]]));
|