exprify 1.0.3 → 1.0.6
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/HISTORY.md +49 -0
- package/README.md +113 -160
- package/SECURITY.md +18 -0
- package/bin/cli.mjs +234 -0
- package/dist/exprify.cjs.cjs +5341 -0
- package/dist/exprify.cjs.cjs.map +1 -0
- package/dist/exprify.esm.js +3558 -1220
- package/dist/exprify.esm.js.map +1 -1
- package/dist/exprify.js +3560 -1222
- package/dist/exprify.js.map +1 -1
- package/dist/exprify.min.js +2 -2
- package/dist/exprify.min.js.map +1 -1
- package/package.json +55 -19
- package/src/core/context.js +35 -27
- package/src/core/exprify.js +880 -0
- package/src/function/executor.js +29 -20
- package/src/function/internal.js +1150 -153
- package/src/function/registry.js +23 -16
- package/src/index.js +1 -1
- package/src/math/bignumber.js +31 -0
- package/src/math/fraction.js +112 -0
- package/src/math/operations.js +38 -24
- package/src/parser/astBuild.js +276 -214
- package/src/parser/evaluator.js +431 -171
- package/src/parser/tokenizer.js +179 -146
- package/src/utils/decimal.js +264 -0
- package/src/utils/globalUnits.js +43 -35
- package/src/utils/matrix.js +14 -14
- package/src/utils/store.js +69 -47
- package/src/variables/store.js +18 -15
- package/dist/exprify.cjs.js +0 -3003
- package/dist/exprify.cjs.js.map +0 -1
- package/src/assets/capture.jpg +0 -0
- package/src/core/Exprify.js +0 -369
package/src/parser/astBuild.js
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
|
+
/** @param {string | any[]} tokens */
|
|
1
2
|
export function buildAST(tokens) {
|
|
2
3
|
let current = 0;
|
|
3
4
|
|
|
4
5
|
const peek = () => tokens[current];
|
|
5
6
|
const consume = () => tokens[current++];
|
|
7
|
+
const lastPos = () => {
|
|
8
|
+
const t = current > 0 ? tokens[current - 1] : null;
|
|
9
|
+
return t && t.pos !== undefined ? t.pos : -1;
|
|
10
|
+
};
|
|
11
|
+
const tokenPos = () => {
|
|
12
|
+
const t = peek();
|
|
13
|
+
return t && t.pos !== undefined ? t.pos : -1;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const nodeAt = (/** @type {any} */ node) => {
|
|
17
|
+
const pos = lastPos();
|
|
18
|
+
if (pos >= 0) {
|
|
19
|
+
node.pos = pos;
|
|
20
|
+
}
|
|
21
|
+
return node;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const syntaxError = (/** @type {string} */ msg) => {
|
|
25
|
+
const pos = tokenPos() >= 0 ? tokenPos() : lastPos();
|
|
26
|
+
const at = pos >= 0 ? ` at position ${pos}` : '';
|
|
27
|
+
throw new Error(`${msg}${at}`);
|
|
28
|
+
};
|
|
6
29
|
|
|
7
|
-
const match = (type, value) => {
|
|
30
|
+
const match = (/** @type {string} */ type, /** @type {string | undefined} */ value) => {
|
|
8
31
|
const t = peek();
|
|
9
|
-
if (!t)
|
|
32
|
+
if (!t) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
10
35
|
|
|
11
|
-
if (t.type !== type)
|
|
36
|
+
if (t.type !== type) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
12
39
|
|
|
13
|
-
if (value !== undefined && t.value !== value)
|
|
40
|
+
if (value !== undefined && t.value !== value) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
14
43
|
|
|
15
44
|
current++;
|
|
16
45
|
return true;
|
|
@@ -19,203 +48,208 @@ export function buildAST(tokens) {
|
|
|
19
48
|
const parseSliceOrIndex = () => {
|
|
20
49
|
let start = null;
|
|
21
50
|
|
|
22
|
-
if (!(peek()?.type ===
|
|
51
|
+
if (!(peek()?.type === 'Colon' || peek()?.type === 'Comma' || peek()?.type === 'ArrayEnd')) {
|
|
23
52
|
start = parseExpression();
|
|
24
53
|
}
|
|
25
54
|
|
|
26
|
-
if (match(
|
|
55
|
+
if (match('Colon', undefined)) {
|
|
27
56
|
let end = null;
|
|
28
57
|
|
|
29
|
-
if (!(peek()?.type ===
|
|
58
|
+
if (!(peek()?.type === 'Comma' || peek()?.type === 'ArrayEnd')) {
|
|
30
59
|
end = parseExpression();
|
|
31
60
|
}
|
|
32
61
|
|
|
33
62
|
return {
|
|
34
|
-
type:
|
|
63
|
+
type: 'SliceExpression',
|
|
35
64
|
start,
|
|
36
|
-
end
|
|
65
|
+
end,
|
|
37
66
|
};
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
return start;
|
|
41
70
|
};
|
|
42
71
|
|
|
43
|
-
/* ================= PRIMARY ================= */
|
|
44
72
|
function parsePrimary() {
|
|
45
73
|
const token = consume();
|
|
46
|
-
if (!token)
|
|
74
|
+
if (!token) {
|
|
75
|
+
syntaxError('Unexpected end of input');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const withPos = (/** @type {any} */ node) => {
|
|
79
|
+
if (token.pos !== undefined) {
|
|
80
|
+
node.pos = token.pos;
|
|
81
|
+
}
|
|
82
|
+
return node;
|
|
83
|
+
};
|
|
47
84
|
|
|
48
85
|
switch (token.type) {
|
|
49
|
-
case
|
|
50
|
-
case
|
|
51
|
-
case
|
|
52
|
-
case
|
|
53
|
-
return { type:
|
|
54
|
-
|
|
55
|
-
case
|
|
56
|
-
return { type:
|
|
57
|
-
|
|
58
|
-
case
|
|
59
|
-
return {
|
|
60
|
-
type:
|
|
86
|
+
case 'Number':
|
|
87
|
+
case 'BigInt':
|
|
88
|
+
case 'Boolean':
|
|
89
|
+
case 'String':
|
|
90
|
+
return withPos({ type: 'Literal', value: token.value });
|
|
91
|
+
|
|
92
|
+
case 'ImaginaryLiteral':
|
|
93
|
+
return withPos({ type: 'ImaginaryLiteral', value: token.value });
|
|
94
|
+
|
|
95
|
+
case 'NumberWithUnit':
|
|
96
|
+
return withPos({
|
|
97
|
+
type: 'UnitLiteral',
|
|
61
98
|
value: token.value,
|
|
62
|
-
unit: token.unit
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
case
|
|
66
|
-
return { type:
|
|
67
|
-
|
|
68
|
-
case
|
|
69
|
-
return {
|
|
70
|
-
type:
|
|
71
|
-
name: token.name
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
case
|
|
75
|
-
if (token.value ===
|
|
99
|
+
unit: token.unit,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
case 'Identifier':
|
|
103
|
+
return withPos({ type: 'Identifier', name: token.name });
|
|
104
|
+
|
|
105
|
+
case 'Function':
|
|
106
|
+
return withPos({
|
|
107
|
+
type: 'Identifier',
|
|
108
|
+
name: token.name,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
case 'Parenthesis':
|
|
112
|
+
if (token.value === '(') {
|
|
76
113
|
const expr = parseExpression();
|
|
77
114
|
|
|
78
|
-
if (!match(
|
|
79
|
-
|
|
115
|
+
if (!match('Parenthesis', ')')) {
|
|
116
|
+
syntaxError("Expected ')'");
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
return expr;
|
|
83
120
|
}
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
|
|
122
|
+
// falls through
|
|
123
|
+
|
|
124
|
+
case 'ArrayStart': {
|
|
86
125
|
const rows = [];
|
|
87
126
|
let currentRow = [];
|
|
88
127
|
|
|
89
|
-
if (!match(
|
|
128
|
+
if (!match('ArrayEnd', undefined)) {
|
|
90
129
|
while (true) {
|
|
91
130
|
currentRow.push(parseExpression());
|
|
92
131
|
|
|
93
|
-
if (match(
|
|
132
|
+
if (match('Comma', undefined)) {
|
|
94
133
|
continue;
|
|
95
134
|
}
|
|
96
135
|
|
|
97
|
-
if (match(
|
|
136
|
+
if (match('Semicolon', undefined)) {
|
|
98
137
|
rows.push(currentRow);
|
|
99
138
|
currentRow = [];
|
|
100
139
|
continue;
|
|
101
140
|
}
|
|
102
141
|
|
|
103
|
-
if (match(
|
|
142
|
+
if (match('ArrayEnd', undefined)) {
|
|
104
143
|
rows.push(currentRow);
|
|
105
144
|
break;
|
|
106
145
|
}
|
|
107
146
|
|
|
108
|
-
|
|
147
|
+
syntaxError("Expected ',', ';', or ']'");
|
|
109
148
|
}
|
|
110
149
|
}
|
|
111
150
|
|
|
112
151
|
if (!rows.length) {
|
|
113
|
-
return { type:
|
|
152
|
+
return withPos({ type: 'ArrayExpression', elements: [] });
|
|
114
153
|
}
|
|
115
154
|
|
|
116
155
|
if (rows.length === 1) {
|
|
117
|
-
return { type:
|
|
156
|
+
return withPos({ type: 'ArrayExpression', elements: rows[0] });
|
|
118
157
|
}
|
|
119
158
|
|
|
120
|
-
return {
|
|
121
|
-
type:
|
|
159
|
+
return withPos({
|
|
160
|
+
type: 'ArrayExpression',
|
|
122
161
|
elements: rows.map((elements) => ({
|
|
123
|
-
type:
|
|
124
|
-
elements
|
|
125
|
-
}))
|
|
126
|
-
};
|
|
162
|
+
type: 'ArrayExpression',
|
|
163
|
+
elements,
|
|
164
|
+
})),
|
|
165
|
+
});
|
|
127
166
|
}
|
|
128
167
|
|
|
129
|
-
case
|
|
168
|
+
case 'BlockStart': {
|
|
130
169
|
const properties = [];
|
|
131
170
|
|
|
132
|
-
if (!match(
|
|
171
|
+
if (!match('BlockEnd', undefined)) {
|
|
133
172
|
do {
|
|
134
173
|
const keyToken = consume();
|
|
135
174
|
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
keyToken.type !== "String"
|
|
139
|
-
) {
|
|
140
|
-
throw new Error("Invalid object key");
|
|
175
|
+
if (keyToken.type !== 'Identifier' && keyToken.type !== 'String') {
|
|
176
|
+
syntaxError('Invalid object key');
|
|
141
177
|
}
|
|
142
178
|
|
|
143
|
-
if (!match(
|
|
144
|
-
|
|
179
|
+
if (!match('Colon', undefined)) {
|
|
180
|
+
syntaxError("Expected ':' after key");
|
|
145
181
|
}
|
|
146
182
|
|
|
147
183
|
const value = parseExpression();
|
|
148
184
|
|
|
149
185
|
properties.push({
|
|
150
186
|
key: keyToken.value,
|
|
151
|
-
value
|
|
187
|
+
value,
|
|
152
188
|
});
|
|
189
|
+
} while (match('Comma', undefined));
|
|
153
190
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!match("BlockEnd")) {
|
|
157
|
-
throw new Error(`Expected '}' at ${current}`);
|
|
191
|
+
if (!match('BlockEnd', undefined)) {
|
|
192
|
+
syntaxError("Expected '}'");
|
|
158
193
|
}
|
|
159
194
|
}
|
|
160
195
|
|
|
161
|
-
return { type:
|
|
196
|
+
return withPos({ type: 'ObjectExpression', properties });
|
|
162
197
|
}
|
|
163
198
|
}
|
|
164
199
|
|
|
165
|
-
|
|
200
|
+
syntaxError(`Unexpected token: ${JSON.stringify(token.value || token.name || token.type)}`);
|
|
166
201
|
}
|
|
167
202
|
|
|
168
|
-
/* ================= MEMBER ================= */
|
|
169
203
|
function parseMember() {
|
|
170
204
|
let object = parsePrimary();
|
|
171
205
|
|
|
172
206
|
while (true) {
|
|
173
|
-
if (match(
|
|
207
|
+
if (match('ArrayStart', undefined)) {
|
|
174
208
|
const selectors = [];
|
|
175
209
|
|
|
176
|
-
if (!match(
|
|
210
|
+
if (!match('ArrayEnd', undefined)) {
|
|
177
211
|
do {
|
|
178
212
|
selectors.push(parseSliceOrIndex());
|
|
179
|
-
} while (match(
|
|
213
|
+
} while (match('Comma', undefined));
|
|
180
214
|
|
|
181
|
-
if (!match(
|
|
182
|
-
|
|
215
|
+
if (!match('ArrayEnd', undefined)) {
|
|
216
|
+
syntaxError("Expected ']'");
|
|
183
217
|
}
|
|
184
218
|
}
|
|
185
219
|
|
|
186
|
-
object = {
|
|
187
|
-
type:
|
|
220
|
+
object = nodeAt({
|
|
221
|
+
type: 'IndexExpression',
|
|
188
222
|
object,
|
|
189
|
-
selectors
|
|
190
|
-
};
|
|
223
|
+
selectors,
|
|
224
|
+
});
|
|
191
225
|
continue;
|
|
192
226
|
}
|
|
193
227
|
|
|
194
|
-
if (match(
|
|
228
|
+
if (match('Dot', undefined)) {
|
|
195
229
|
const property = consume();
|
|
196
230
|
|
|
197
|
-
if (property.type !==
|
|
198
|
-
|
|
231
|
+
if (property.type !== 'Identifier') {
|
|
232
|
+
syntaxError("Expected property after '.'");
|
|
199
233
|
}
|
|
200
234
|
|
|
201
|
-
object = {
|
|
202
|
-
type:
|
|
235
|
+
object = nodeAt({
|
|
236
|
+
type: 'MemberExpression',
|
|
203
237
|
object,
|
|
204
|
-
property: { type:
|
|
205
|
-
optional: false
|
|
206
|
-
};
|
|
238
|
+
property: { type: 'Identifier', name: property.value },
|
|
239
|
+
optional: false,
|
|
240
|
+
});
|
|
207
241
|
continue;
|
|
208
242
|
}
|
|
209
243
|
|
|
210
|
-
if (match(
|
|
244
|
+
if (match('Operator', '?.')) {
|
|
211
245
|
const property = consume();
|
|
212
246
|
|
|
213
|
-
object = {
|
|
214
|
-
type:
|
|
247
|
+
object = nodeAt({
|
|
248
|
+
type: 'MemberExpression',
|
|
215
249
|
object,
|
|
216
|
-
property: { type:
|
|
217
|
-
optional: true
|
|
218
|
-
};
|
|
250
|
+
property: { type: 'Identifier', name: property.value },
|
|
251
|
+
optional: true,
|
|
252
|
+
});
|
|
219
253
|
continue;
|
|
220
254
|
}
|
|
221
255
|
|
|
@@ -225,296 +259,322 @@ export function buildAST(tokens) {
|
|
|
225
259
|
return object;
|
|
226
260
|
}
|
|
227
261
|
|
|
228
|
-
/* ================= CALL ================= */
|
|
229
262
|
function parseCallChain() {
|
|
230
263
|
let expr = parseMember();
|
|
231
264
|
|
|
232
|
-
while (peek()?.type ===
|
|
233
|
-
consume();
|
|
265
|
+
while (peek()?.type === 'Parenthesis' && peek()?.value === '(') {
|
|
266
|
+
consume();
|
|
234
267
|
|
|
235
268
|
const args = [];
|
|
236
269
|
|
|
237
|
-
if (!(peek()?.type ===
|
|
270
|
+
if (!(peek()?.type === 'Parenthesis' && peek()?.value === ')')) {
|
|
238
271
|
do {
|
|
239
|
-
|
|
240
|
-
|
|
272
|
+
if (match('Spread', undefined)) {
|
|
273
|
+
const arg = parseExpression();
|
|
274
|
+
args.push({ type: 'SpreadElement', argument: arg });
|
|
275
|
+
} else {
|
|
276
|
+
args.push(parseExpression());
|
|
277
|
+
}
|
|
278
|
+
} while (match('Comma', undefined));
|
|
241
279
|
}
|
|
242
280
|
|
|
243
|
-
if (!match(
|
|
244
|
-
|
|
281
|
+
if (!match('Parenthesis', ')')) {
|
|
282
|
+
syntaxError("Expected ')'");
|
|
245
283
|
}
|
|
246
284
|
|
|
247
|
-
expr = {
|
|
248
|
-
type:
|
|
285
|
+
expr = nodeAt({
|
|
286
|
+
type: 'CallExpression',
|
|
249
287
|
callee: expr,
|
|
250
|
-
arguments: args
|
|
251
|
-
};
|
|
288
|
+
arguments: args,
|
|
289
|
+
});
|
|
252
290
|
}
|
|
253
291
|
|
|
254
292
|
return expr;
|
|
255
293
|
}
|
|
256
294
|
|
|
257
|
-
/* ================= UNARY ================= */
|
|
258
295
|
function parseUnary() {
|
|
259
|
-
if (match(
|
|
296
|
+
if (match('UnaryOperator', undefined)) {
|
|
260
297
|
const operator = tokens[current - 1].value;
|
|
261
298
|
|
|
262
|
-
return {
|
|
263
|
-
type:
|
|
299
|
+
return nodeAt({
|
|
300
|
+
type: 'UnaryExpression',
|
|
264
301
|
operator,
|
|
265
|
-
argument: parseUnary()
|
|
266
|
-
};
|
|
302
|
+
argument: parseUnary(),
|
|
303
|
+
});
|
|
267
304
|
}
|
|
268
305
|
|
|
269
306
|
return parseCallChain();
|
|
270
307
|
}
|
|
271
308
|
|
|
272
|
-
/* ================= POWER ================= */
|
|
273
309
|
function parsePower() {
|
|
274
|
-
|
|
310
|
+
const left = parseUnary();
|
|
275
311
|
|
|
276
|
-
if (match(
|
|
312
|
+
if (match('Operator', '^')) {
|
|
277
313
|
const right = parsePower();
|
|
278
|
-
return {
|
|
279
|
-
type:
|
|
280
|
-
operator:
|
|
314
|
+
return nodeAt({
|
|
315
|
+
type: 'BinaryExpression',
|
|
316
|
+
operator: '^',
|
|
281
317
|
left,
|
|
282
|
-
right
|
|
283
|
-
};
|
|
318
|
+
right,
|
|
319
|
+
});
|
|
284
320
|
}
|
|
285
321
|
|
|
286
322
|
return left;
|
|
287
323
|
}
|
|
288
324
|
|
|
289
|
-
/* ================= MULT ================= */
|
|
290
325
|
function parseMultiplication() {
|
|
291
326
|
let left = parsePower();
|
|
292
327
|
|
|
293
|
-
while (
|
|
294
|
-
match("Operator", "*") ||
|
|
295
|
-
match("Operator", "/") ||
|
|
296
|
-
match("Operator", "%")
|
|
297
|
-
) {
|
|
328
|
+
while (match('Operator', '*') || match('Operator', '/') || match('Operator', '%')) {
|
|
298
329
|
const operator = tokens[current - 1].value;
|
|
299
330
|
const right = parsePower();
|
|
300
331
|
|
|
301
|
-
left = {
|
|
302
|
-
type:
|
|
332
|
+
left = nodeAt({
|
|
333
|
+
type: 'BinaryExpression',
|
|
303
334
|
operator,
|
|
304
335
|
left,
|
|
305
|
-
right
|
|
306
|
-
};
|
|
336
|
+
right,
|
|
337
|
+
});
|
|
307
338
|
}
|
|
308
339
|
|
|
309
340
|
return left;
|
|
310
341
|
}
|
|
311
342
|
|
|
312
|
-
/* ================= ADD ================= */
|
|
313
343
|
function parseAddition() {
|
|
314
344
|
let left = parseMultiplication();
|
|
315
345
|
|
|
316
|
-
while (match(
|
|
346
|
+
while (match('Operator', '+') || match('Operator', '-')) {
|
|
317
347
|
const operator = tokens[current - 1].value;
|
|
318
348
|
const right = parseMultiplication();
|
|
319
349
|
|
|
320
|
-
left = {
|
|
321
|
-
type:
|
|
350
|
+
left = nodeAt({
|
|
351
|
+
type: 'BinaryExpression',
|
|
322
352
|
operator,
|
|
323
353
|
left,
|
|
324
|
-
right
|
|
325
|
-
};
|
|
354
|
+
right,
|
|
355
|
+
});
|
|
326
356
|
}
|
|
327
357
|
|
|
328
358
|
return left;
|
|
329
359
|
}
|
|
330
360
|
|
|
331
|
-
/* ================= UNIT CONVERSION ================= */
|
|
332
361
|
function parseUnitConversion() {
|
|
333
|
-
|
|
362
|
+
const left = parseAddition();
|
|
334
363
|
|
|
335
364
|
const nextKeyword = peek();
|
|
336
|
-
if (nextKeyword?.type ===
|
|
365
|
+
if (nextKeyword?.type === 'Keyword' && ['to', 'in'].includes(nextKeyword.value)) {
|
|
337
366
|
consume();
|
|
338
367
|
const next = consume();
|
|
339
368
|
|
|
340
|
-
if (!next || next.type !==
|
|
341
|
-
|
|
369
|
+
if (!next || next.type !== 'Unit') {
|
|
370
|
+
syntaxError(`Expected unit after '${nextKeyword.value}'`);
|
|
342
371
|
}
|
|
343
372
|
|
|
344
|
-
return {
|
|
345
|
-
type:
|
|
373
|
+
return nodeAt({
|
|
374
|
+
type: 'UnitConversion',
|
|
346
375
|
from: left,
|
|
347
|
-
to: next.value
|
|
348
|
-
};
|
|
376
|
+
to: next.value,
|
|
377
|
+
});
|
|
349
378
|
}
|
|
350
379
|
|
|
351
380
|
return left;
|
|
352
381
|
}
|
|
353
382
|
|
|
354
|
-
/* ================= COMPARISON ================= */
|
|
355
383
|
function parseComparison() {
|
|
356
384
|
let left = parseUnitConversion();
|
|
357
385
|
|
|
358
386
|
while (
|
|
359
|
-
match(
|
|
360
|
-
match(
|
|
361
|
-
match(
|
|
362
|
-
match(
|
|
363
|
-
match(
|
|
387
|
+
match('Operator', '>') ||
|
|
388
|
+
match('Operator', '<') ||
|
|
389
|
+
match('Operator', '>=') ||
|
|
390
|
+
match('Operator', '<=') ||
|
|
391
|
+
match('Operator', '==')
|
|
364
392
|
) {
|
|
365
393
|
const operator = tokens[current - 1].value;
|
|
366
394
|
const right = parseUnitConversion();
|
|
367
395
|
|
|
368
|
-
left = {
|
|
369
|
-
type:
|
|
396
|
+
left = nodeAt({
|
|
397
|
+
type: 'BinaryExpression',
|
|
370
398
|
operator,
|
|
371
399
|
left,
|
|
372
|
-
right
|
|
373
|
-
};
|
|
400
|
+
right,
|
|
401
|
+
});
|
|
374
402
|
}
|
|
375
403
|
|
|
376
404
|
return left;
|
|
377
405
|
}
|
|
378
406
|
|
|
379
|
-
/* ================= LOGICAL ================= */
|
|
380
407
|
function parseLogical() {
|
|
381
408
|
let left = parseComparison();
|
|
382
409
|
|
|
383
|
-
while (
|
|
384
|
-
match("Operator", "&&") ||
|
|
385
|
-
match("Operator", "||")
|
|
386
|
-
) {
|
|
410
|
+
while (match('Operator', '&&') || match('Operator', '||')) {
|
|
387
411
|
const operator = tokens[current - 1].value;
|
|
388
412
|
const right = parseComparison();
|
|
389
413
|
|
|
390
|
-
left = {
|
|
391
|
-
type:
|
|
414
|
+
left = nodeAt({
|
|
415
|
+
type: 'LogicalExpression',
|
|
392
416
|
operator,
|
|
393
417
|
left,
|
|
394
|
-
right
|
|
395
|
-
};
|
|
418
|
+
right,
|
|
419
|
+
});
|
|
396
420
|
}
|
|
397
421
|
|
|
398
422
|
return left;
|
|
399
423
|
}
|
|
400
424
|
|
|
401
|
-
/* ================= NULLISH ================= */
|
|
402
425
|
function parseNullish() {
|
|
403
426
|
let left = parseLogical();
|
|
404
427
|
|
|
405
|
-
while (match(
|
|
428
|
+
while (match('Operator', '??')) {
|
|
406
429
|
const right = parseLogical();
|
|
407
430
|
|
|
408
|
-
left = {
|
|
409
|
-
type:
|
|
410
|
-
operator:
|
|
431
|
+
left = nodeAt({
|
|
432
|
+
type: 'LogicalExpression',
|
|
433
|
+
operator: '??',
|
|
411
434
|
left,
|
|
412
|
-
right
|
|
413
|
-
};
|
|
435
|
+
right,
|
|
436
|
+
});
|
|
414
437
|
}
|
|
415
438
|
|
|
416
439
|
return left;
|
|
417
440
|
}
|
|
418
441
|
|
|
419
|
-
/* ================= TERNARY ================= */
|
|
420
442
|
function parseTernary() {
|
|
421
|
-
|
|
443
|
+
const test = parseNullish();
|
|
422
444
|
|
|
423
|
-
if (match(
|
|
445
|
+
if (match('Ternary', '?')) {
|
|
424
446
|
const consequent = parseExpression();
|
|
425
447
|
|
|
426
|
-
if (!match(
|
|
427
|
-
|
|
448
|
+
if (!match('Ternary', ':')) {
|
|
449
|
+
syntaxError("Expected ':' in ternary");
|
|
428
450
|
}
|
|
429
451
|
|
|
430
452
|
const alternate = parseExpression();
|
|
431
453
|
|
|
432
|
-
return {
|
|
433
|
-
type:
|
|
454
|
+
return nodeAt({
|
|
455
|
+
type: 'ConditionalExpression',
|
|
434
456
|
test,
|
|
435
457
|
consequent,
|
|
436
|
-
alternate
|
|
437
|
-
};
|
|
458
|
+
alternate,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (match('Colon', undefined)) {
|
|
463
|
+
const end = parseNullish();
|
|
464
|
+
|
|
465
|
+
return nodeAt({
|
|
466
|
+
type: 'RangeExpression',
|
|
467
|
+
start: test,
|
|
468
|
+
end,
|
|
469
|
+
});
|
|
438
470
|
}
|
|
439
471
|
|
|
440
472
|
return test;
|
|
441
473
|
}
|
|
442
474
|
|
|
443
|
-
|
|
475
|
+
function parseLambda() {
|
|
476
|
+
const left = parsePipeline();
|
|
477
|
+
|
|
478
|
+
if (match('Operator', '->')) {
|
|
479
|
+
let params;
|
|
480
|
+
if (left.type === 'Identifier') {
|
|
481
|
+
params = [left.name];
|
|
482
|
+
} else if (left.type === 'ArrayExpression') {
|
|
483
|
+
params = left.elements.map((/** @type {{ type: string; name: any; }} */ el) => {
|
|
484
|
+
if (el.type !== 'Identifier') {
|
|
485
|
+
syntaxError('Lambda parameter must be an identifier');
|
|
486
|
+
}
|
|
487
|
+
return el.name;
|
|
488
|
+
});
|
|
489
|
+
} else {
|
|
490
|
+
syntaxError('Invalid lambda parameter');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const body = parseLambda();
|
|
494
|
+
|
|
495
|
+
return nodeAt({
|
|
496
|
+
type: 'ArrowFunctionExpression',
|
|
497
|
+
params,
|
|
498
|
+
body,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return left;
|
|
503
|
+
}
|
|
504
|
+
|
|
444
505
|
function parsePipeline() {
|
|
445
506
|
let left = parseTernary();
|
|
446
507
|
|
|
447
|
-
while (match(
|
|
508
|
+
while (match('Operator', '|>')) {
|
|
448
509
|
const right = parseTernary();
|
|
449
510
|
|
|
450
|
-
left = {
|
|
451
|
-
type:
|
|
511
|
+
left = nodeAt({
|
|
512
|
+
type: 'PipelineExpression',
|
|
452
513
|
left,
|
|
453
|
-
right
|
|
454
|
-
};
|
|
514
|
+
right,
|
|
515
|
+
});
|
|
455
516
|
}
|
|
456
517
|
|
|
457
518
|
return left;
|
|
458
519
|
}
|
|
459
520
|
|
|
460
|
-
/* ================= ASSIGNMENT ================= */
|
|
461
521
|
function parseAssignment() {
|
|
462
|
-
|
|
522
|
+
const left = parseLambda();
|
|
463
523
|
|
|
464
524
|
if (
|
|
465
|
-
match(
|
|
466
|
-
match(
|
|
467
|
-
match(
|
|
468
|
-
match(
|
|
469
|
-
match(
|
|
525
|
+
match('Operator', '=') ||
|
|
526
|
+
match('Operator', '+=') ||
|
|
527
|
+
match('Operator', '-=') ||
|
|
528
|
+
match('Operator', '*=') ||
|
|
529
|
+
match('Operator', '/=')
|
|
470
530
|
) {
|
|
471
531
|
const operator = tokens[current - 1].value;
|
|
472
532
|
|
|
473
|
-
|
|
533
|
+
// f(a,b) = expr: treat as function definition, not assignment
|
|
534
|
+
if (left.type === 'CallExpression') {
|
|
474
535
|
const isFunctionTarget =
|
|
475
|
-
left.callee?.type ===
|
|
476
|
-
left.arguments.every((arg) => arg.type ===
|
|
536
|
+
left.callee?.type === 'Identifier' &&
|
|
537
|
+
left.arguments.every((/** @type {{ type: string; }} */ arg) => arg.type === 'Identifier');
|
|
477
538
|
|
|
478
539
|
if (!isFunctionTarget) {
|
|
479
|
-
|
|
540
|
+
syntaxError('Invalid function definition');
|
|
480
541
|
}
|
|
481
542
|
|
|
482
543
|
const right = parseAssignment();
|
|
483
544
|
|
|
484
|
-
return {
|
|
485
|
-
type:
|
|
545
|
+
return nodeAt({
|
|
546
|
+
type: 'FunctionAssignmentExpression',
|
|
486
547
|
operator,
|
|
487
548
|
left: {
|
|
488
|
-
type:
|
|
489
|
-
name: left.callee.name
|
|
549
|
+
type: 'Identifier',
|
|
550
|
+
name: left.callee.name,
|
|
490
551
|
},
|
|
491
|
-
params: left.arguments.map((arg) => arg.name),
|
|
492
|
-
right
|
|
493
|
-
};
|
|
552
|
+
params: left.arguments.map((/** @type {{ name: any; }} */ arg) => arg.name),
|
|
553
|
+
right,
|
|
554
|
+
});
|
|
494
555
|
}
|
|
495
556
|
|
|
496
557
|
if (
|
|
497
|
-
left.type !==
|
|
498
|
-
left.type !==
|
|
499
|
-
left.type !==
|
|
558
|
+
left.type !== 'Identifier' &&
|
|
559
|
+
left.type !== 'MemberExpression' &&
|
|
560
|
+
left.type !== 'IndexExpression'
|
|
500
561
|
) {
|
|
501
|
-
|
|
562
|
+
syntaxError('Invalid assignment target');
|
|
502
563
|
}
|
|
503
564
|
|
|
504
565
|
const right = parseAssignment();
|
|
505
566
|
|
|
506
|
-
return {
|
|
507
|
-
type:
|
|
567
|
+
return nodeAt({
|
|
568
|
+
type: 'AssignmentExpression',
|
|
508
569
|
operator,
|
|
509
570
|
left,
|
|
510
|
-
right
|
|
511
|
-
};
|
|
571
|
+
right,
|
|
572
|
+
});
|
|
512
573
|
}
|
|
513
574
|
|
|
514
575
|
return left;
|
|
515
576
|
}
|
|
516
577
|
|
|
517
|
-
/* ================= ENTRY ================= */
|
|
518
578
|
function parseExpression() {
|
|
519
579
|
return parseAssignment();
|
|
520
580
|
}
|
|
@@ -522,8 +582,10 @@ export function buildAST(tokens) {
|
|
|
522
582
|
const ast = parseExpression();
|
|
523
583
|
|
|
524
584
|
if (current < tokens.length) {
|
|
585
|
+
const t = peek();
|
|
586
|
+
const pos = t && t.pos !== undefined ? ` at position ${t.pos}` : '';
|
|
525
587
|
throw new Error(
|
|
526
|
-
`Unexpected token
|
|
588
|
+
`Unexpected token "${t ? JSON.stringify(t.value || t.name || t.type) : '?'}"${pos}`
|
|
527
589
|
);
|
|
528
590
|
}
|
|
529
591
|
|