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.
Files changed (83) hide show
  1. package/README.md +115 -169
  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 -5
  10. package/feature/destruct.js +33 -0
  11. package/feature/function.js +44 -0
  12. package/feature/group.js +39 -9
  13. package/feature/if.js +23 -38
  14. package/feature/literal.js +13 -0
  15. package/feature/loop.js +107 -106
  16. package/feature/module.js +42 -0
  17. package/feature/number.js +41 -38
  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 -17
  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 +51 -41
  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 -4
  52. package/package.json +15 -16
  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 -4
  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/control.js +0 -142
  68. package/feature/increment.js +0 -11
  69. package/feature/logic.js +0 -11
  70. package/feature/mult.js +0 -25
  71. package/feature/object.js +0 -17
  72. package/feature/optional.js +0 -23
  73. package/feature/pow.js +0 -5
  74. package/feature/shift.js +0 -12
  75. package/feature/spread.js +0 -6
  76. package/feature/ternary.js +0 -10
  77. package/src/compile.d.ts +0 -17
  78. package/src/compile.js +0 -28
  79. package/src/const.js +0 -45
  80. package/src/parse.d.ts +0 -22
  81. package/src/parse.js +0 -113
  82. package/src/stringify.js +0 -27
  83. /package/{LICENSE → license} +0 -0
package/README.md CHANGED
@@ -1,97 +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.
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
9
- * sandboxes, playgrounds, safe eval
10
- * custom DSL
11
- * preprocessors
12
- * templates
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_) -->, good [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`
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
- 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] }
70
43
  ```
71
44
 
72
- ### Control Flow
73
-
74
- For statement syntax, import `subscript/feature/control.js`:
45
+ ### jessie
75
46
 
76
- + `if (c) a`, `if (c) a else b`
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 subscript from 'subscript/justin'
85
- import 'subscript/feature/control.js'
86
-
87
- let sum = subscript(`
88
- let sum = 0;
89
- for (i = 0; i < 10; i += 1) sum += i;
90
- 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)
91
59
  `)
92
- sum() // 45
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
- ### Syntax Tree
82
+ ## Extension
113
83
 
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):
115
-
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:
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 { compile } from 'subscript.js'
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
- ### Stringify
100
+ See [docs.md](./docs.md) for full API.
101
+
147
102
 
148
- To convert tree back to code, there's codegenerator function:
103
+ ## Syntax Tree
104
+
105
+ Expressions parse to a minimal JSON-compatible AST:
149
106
 
150
107
  ```js
151
- import { stringify } from 'subscript.js'
108
+ import { parse } from 'subscript'
152
109
 
153
- stringify(['+', ['*', 'min', [,60]], [,'sec']])
154
- // 'min * 60 + "sec"'
110
+ parse('a + b * 2')
111
+ // ['+', 'a', ['*', 'b', [, 2]]]
155
112
  ```
156
113
 
157
- ## Extending
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
- * `unary(str, precedence, postfix=false)` register unary operator, either prefix `⚬a` or postfix `a⚬`.
162
- * `binary(str, precedence, rassoc=false)` register binary operator `a ⚬ b`, optionally right-associative.
163
- * `nary(str, precedence)` register n-ary (sequence) operator like `a; b;` or `a, b`, allows missing args.
164
- * `group(str, precedence)` - register group, like `[a]`, `{a}`, `(a)` etc.
165
- * `access(str, precedence)` - register access operator, like `a[b]`, `a(b)` etc.
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
- Longer operators should be registered after shorter ones, eg. first `|`, then `||`, then `||=`.
122
+ Three forms:
170
123
 
171
124
  ```js
172
- 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
+ ```
173
129
 
174
- // enable objects/arrays syntax
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
- // add nullish coalescing (precedence of logical or)
184
- binary('??', 3)
185
- operator('??', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) ?? b(ctx)))
133
+ ## Safety
186
134
 
187
- // add JS literals
188
- token('undefined', 20, a => a ? err() : [, undefined])
189
- 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)
190
143
  ```
191
144
 
192
- See [`./feature/*`](./feature) or [`./justin.js`](./justin.js) for examples.
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
- ## Performance
154
+ ### Codegen
197
155
 
198
- Subscript shows good performance within other evaluators. Example expression:
156
+ Convert tree back to code:
199
157
 
200
- ```
201
- 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"'
202
163
  ```
203
164
 
204
- Parse 30k times:
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
- 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: -
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
- [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)
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
- ## 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/).
242
188
 
243
- [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) -->
244
190
 
245
- <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, err } from '../src/parse.js'
2
- import { operator, compile } from '../src/compile.js'
3
- import { PREC_ACCESS, unsafe } 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 => { const k = b(ctx); return unsafe(k) ? undefined : a(ctx)[k] }))
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, unsafe(b) ? () => undefined : ctx => a(ctx)[b]))
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)]]));