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.
Files changed (82) hide show
  1. package/README.md +123 -171
  2. package/feature/access.js +67 -7
  3. package/feature/accessor.js +49 -0
  4. package/feature/asi.js +15 -0
  5. package/feature/async.js +45 -0
  6. package/feature/block.js +41 -0
  7. package/feature/class.js +69 -0
  8. package/feature/collection.js +40 -0
  9. package/feature/comment.js +25 -6
  10. package/feature/destruct.js +33 -0
  11. package/feature/function.js +44 -0
  12. package/feature/group.js +41 -12
  13. package/feature/if.js +28 -0
  14. package/feature/literal.js +13 -0
  15. package/feature/loop.js +123 -0
  16. package/feature/module.js +42 -0
  17. package/feature/number.js +45 -10
  18. package/feature/op/arithmetic.js +29 -0
  19. package/feature/op/arrow.js +33 -0
  20. package/feature/op/assign-logical.js +33 -0
  21. package/feature/op/assignment.js +47 -0
  22. package/feature/op/bitwise-unsigned.js +17 -0
  23. package/feature/op/bitwise.js +29 -0
  24. package/feature/op/comparison.js +19 -0
  25. package/feature/op/defer.js +15 -0
  26. package/feature/op/equality.js +16 -0
  27. package/feature/op/identity.js +15 -0
  28. package/feature/op/increment.js +23 -0
  29. package/feature/op/logical.js +21 -0
  30. package/feature/op/membership.js +17 -0
  31. package/feature/op/nullish.js +13 -0
  32. package/feature/op/optional.js +61 -0
  33. package/feature/op/pow.js +19 -0
  34. package/feature/op/range.js +26 -0
  35. package/feature/op/spread.js +15 -0
  36. package/feature/op/ternary.js +15 -0
  37. package/feature/op/type.js +18 -0
  38. package/feature/op/unary.js +41 -0
  39. package/feature/prop.js +34 -0
  40. package/feature/regex.js +31 -0
  41. package/feature/seq.js +21 -0
  42. package/feature/string.js +24 -16
  43. package/feature/switch.js +48 -0
  44. package/feature/template.js +39 -0
  45. package/feature/try.js +57 -0
  46. package/feature/unit.js +35 -0
  47. package/feature/var.js +59 -0
  48. package/jessie.js +31 -0
  49. package/jessie.min.js +8 -0
  50. package/justin.js +39 -48
  51. package/justin.min.js +8 -1
  52. package/package.json +18 -23
  53. package/parse.js +153 -0
  54. package/subscript.d.ts +45 -5
  55. package/subscript.js +62 -22
  56. package/subscript.min.js +5 -1
  57. package/util/bundle.js +507 -0
  58. package/util/stringify.js +172 -0
  59. package/feature/add.js +0 -22
  60. package/feature/array.js +0 -11
  61. package/feature/arrow.js +0 -23
  62. package/feature/assign.js +0 -11
  63. package/feature/bitwise.js +0 -11
  64. package/feature/bool.js +0 -5
  65. package/feature/call.js +0 -15
  66. package/feature/compare.js +0 -11
  67. package/feature/increment.js +0 -11
  68. package/feature/logic.js +0 -11
  69. package/feature/mult.js +0 -25
  70. package/feature/object.js +0 -17
  71. package/feature/optional.js +0 -30
  72. package/feature/pow.js +0 -5
  73. package/feature/shift.js +0 -12
  74. package/feature/spread.js +0 -6
  75. package/feature/ternary.js +0 -10
  76. package/src/compile.d.ts +0 -17
  77. package/src/compile.js +0 -28
  78. package/src/const.js +0 -42
  79. package/src/parse.d.ts +0 -22
  80. package/src/parse.js +0 -114
  81. package/src/stringify.js +0 -31
  82. /package/{LICENSE → license} +0 -0
package/README.md CHANGED
@@ -1,75 +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)
2
2
 
3
- > _Subscript_ is fast, tiny & extensible parser / evaluator / microlanguage with standard syntax.
3
+ > Tiny expression parser & evaluator.
4
4
 
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
6
10
 
7
- * expressions evaluators, calculators
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
- _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_), best [performance](#performance) and extensive test coverage.
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
- ## Usage
20
+ ## Presets
18
21
 
19
- ```js
20
- import subscript from './subscript.js'
22
+ ### subscript
21
23
 
22
- // parse expression
23
- const fn = subscript('a.b + Math.sqrt(c - 1)')
24
+ Common expressions:
24
25
 
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
28
31
  ```
29
32
 
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`
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 jstin from './justin.js'
39
+ import justin from 'subscript/justin.js'
68
40
 
69
- let xy = jstin('{ x: 1, "y": 2+2 }["x"]')
70
- xy() // 1
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
- ### Syntax Tree
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
- import { compile } from 'subscript.js'
102
-
103
- const fn = compile(['+', ['*', 'min', [,60]], [,'sec']])
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
- ### Stringify
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
- stringify(['+', ['*', 'min', [,60]], [,'sec']])
132
- // 'min*60 + "sec" == "300sec"'
133
- ```
133
+ ## Safety
134
134
 
135
- ## Extending
135
+ Blocked by default:
136
+ - `__proto__`, `__defineGetter__`, `__defineSetter__`
137
+ - `constructor`, `prototype`
138
+ - Global access (only context is visible)
136
139
 
137
- _Subscript_ provides premade language [features](./features) and API to customize syntax:
140
+ ```js
141
+ subscript('constructor.constructor("alert(1)")()')({})
142
+ // undefined (blocked)
143
+ ```
138
144
 
139
- * `unary(str, precedence, postfix=false)` − register unary operator, either prefix `⚬a` or postfix `a⚬`.
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
- Longer operators should be registered after shorter ones, eg. first `|`, then `||`, then `||=`.
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
- ```js
150
- import script, { compile, operator, unary, binary, token } from './subscript.js'
152
+ ## Utils
151
153
 
152
- // enable objects/arrays syntax
153
- import 'subscript/feature/array.js';
154
- import 'subscript/feature/object.js';
154
+ ### Codegen
155
155
 
156
- // add identity operators (precedence of comparison)
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
- // add nullish coalescing (precedence of logical or)
162
- binary('??', 3)
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
- // add JS literals
166
- token('undefined', 20, a => a ? err() : [, undefined])
167
- token('NaN', 20, a => a ? err() : [, NaN])
161
+ codegen(['+', ['*', 'min', [,60]], [,'sec']])
162
+ // 'min * 60 + "sec"'
168
163
  ```
169
164
 
170
- See [`./feature/*`](./feature) or [`./justin.js`](./justin.js) for examples.
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
- ## Performance
167
+ Create custom dialect as single file:
197
168
 
198
- Subscript shows good performance within other evaluators. Example expression:
169
+ ```js
170
+ import { bundle } from 'subscript/util/bundle.js'
199
171
 
200
- ```
201
- 1 + (a * b / c % d) - 2.0 + -3e-3 * +4.4e4 / f.g[0] - i.j(+k == 1)(0)
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
- Eval 30k times:
221
- ```
222
- new Function: ~7 ms 🥇
223
- subscript: ~15 ms 🥈
224
- justin: ~17 ms
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: -
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
- ## 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/).
236
188
 
237
- [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) -->
238
190
 
239
- <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>
package/feature/access.js CHANGED
@@ -1,11 +1,71 @@
1
- import { access, binary, group } from '../src/parse.js'
2
- import { operator, compile } from '../src/compile.js'
3
- import { CBRACK, PREC_ACCESS } from '../src/const.js'
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('[]', PREC_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('.', PREC_ACCESS)
11
- operator('.', (a, b) => (a = compile(a), b = !b[0] ? b[1] : b, ctx => a(ctx)[b])) // a.true, a.1 → needs to work fine
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
+ };
@@ -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) }; }));
@@ -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)));
@@ -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)]]));