exprify 1.0.0 → 1.0.1
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/.gitattributes +2 -0
- package/.github/workflows/ci.yml +40 -0
- package/.github/workflows/npm-publish.yml +38 -0
- package/.github/workflows/security-audit.yml +34 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +673 -673
- package/README.md +203 -135
- package/dist/exprify.cjs.js +2320 -503
- package/dist/exprify.cjs.js.map +1 -1
- package/dist/exprify.esm.js +2320 -497
- package/dist/exprify.esm.js.map +1 -1
- package/dist/exprify.js +2340 -523
- package/dist/exprify.js.map +1 -1
- package/dist/exprify.min.js +2 -2
- package/dist/exprify.min.js.map +1 -1
- package/doc/tokenType.txt +48 -0
- package/package.json +7 -3
- package/rollup.config.js +80 -0
- package/src/assets/capture.jpg +0 -0
- package/src/core/Exprify.js +140 -70
- package/src/core/context.js +30 -0
- package/src/function/executor.js +64 -0
- package/src/function/internal.js +270 -0
- package/src/function/registry.js +68 -0
- package/src/index.js +2 -38
- package/src/math/operations.js +37 -47
- package/src/parser/astBuild.js +508 -0
- package/src/parser/evaluator.js +430 -57
- package/src/parser/tokenizer.js +399 -145
- package/src/utils/globalUnits.js +217 -0
- package/src/utils/store.js +178 -0
- package/src/variables/store.js +75 -0
- package/test/browser.html +23 -0
- package/test/exprify.test.js +140 -0
- package/src/functions/externalFunctions.js +0 -19
- package/src/functions/internalFunctions.js +0 -53
- package/src/parser/infixToPostfix.js +0 -78
- package/src/utils/typeConverter.js +0 -63
- package/src/variables/variables.js +0 -28
package/src/math/operations.js
CHANGED
|
@@ -1,48 +1,38 @@
|
|
|
1
|
-
const isValidNumberPair = (a, b) =>
|
|
2
|
-
(typeof a === typeof b) &&
|
|
3
|
-
(typeof a === 'number' || typeof a === 'bigint');
|
|
4
|
-
|
|
5
|
-
export const mathOperations = Object.freeze({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (isValidNumberPair(a, b))
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
throw new Error("Invalid types for
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (isValidNumberPair(a, b)) return a
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
subtract: function(a, b) {
|
|
40
|
-
if (isValidNumberPair(a, b)) return a - b;
|
|
41
|
-
throw new Error("Invalid types for -");
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
modulus: function(a, b) {
|
|
45
|
-
if (isValidNumberPair(a, b)) return a % b;
|
|
46
|
-
throw new Error("Invalid types for %");
|
|
47
|
-
}
|
|
1
|
+
const isValidNumberPair = (a, b) =>
|
|
2
|
+
(typeof a === typeof b) &&
|
|
3
|
+
(typeof a === 'number' || typeof a === 'bigint');
|
|
4
|
+
|
|
5
|
+
export const mathOperations = Object.freeze({
|
|
6
|
+
power: function(a, b) {
|
|
7
|
+
if (isValidNumberPair(a, b)) return a ** b;
|
|
8
|
+
throw new Error("Invalid types for ^");
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
multiply: function(a, b) {
|
|
12
|
+
if (isValidNumberPair(a, b)) return a * b;
|
|
13
|
+
throw new Error("Invalid types for *");
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
divide: function(a, b) {
|
|
17
|
+
if (isValidNumberPair(a, b)) {
|
|
18
|
+
if (b === 0) throw new Error("Division by zero");
|
|
19
|
+
return a / b;
|
|
20
|
+
}
|
|
21
|
+
throw new Error("Invalid types for /");
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
add: function(a, b) {
|
|
25
|
+
if (isValidNumberPair(a, b)) return a + b;
|
|
26
|
+
if (typeof a === 'string' && typeof b === 'string') return a + b;
|
|
27
|
+
throw new Error("Invalid types for +");
|
|
28
|
+
},
|
|
29
|
+
subtract: function(a, b) {
|
|
30
|
+
if (isValidNumberPair(a, b)) return a - b;
|
|
31
|
+
throw new Error("Invalid types for -");
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
modulus: function(a, b) {
|
|
35
|
+
if (isValidNumberPair(a, b)) return a % b;
|
|
36
|
+
throw new Error("Invalid types for %");
|
|
37
|
+
}
|
|
48
38
|
});
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
export function buildAST(tokens) {
|
|
2
|
+
let current = 0;
|
|
3
|
+
|
|
4
|
+
const peek = () => tokens[current];
|
|
5
|
+
const consume = () => tokens[current++];
|
|
6
|
+
|
|
7
|
+
const match = (type, value) => {
|
|
8
|
+
const t = peek();
|
|
9
|
+
if (!t) return false;
|
|
10
|
+
|
|
11
|
+
if (t.type !== type) return false;
|
|
12
|
+
|
|
13
|
+
if (value !== undefined && t.value !== value) return false;
|
|
14
|
+
|
|
15
|
+
current++;
|
|
16
|
+
return true;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const parseSliceOrIndex = () => {
|
|
20
|
+
let start = null;
|
|
21
|
+
|
|
22
|
+
if (!(peek()?.type === "Colon" || peek()?.type === "Comma" || peek()?.type === "ArrayEnd")) {
|
|
23
|
+
start = parseExpression();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (match("Colon")) {
|
|
27
|
+
let end = null;
|
|
28
|
+
|
|
29
|
+
if (!(peek()?.type === "Comma" || peek()?.type === "ArrayEnd")) {
|
|
30
|
+
end = parseExpression();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
type: "SliceExpression",
|
|
35
|
+
start,
|
|
36
|
+
end
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return start;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/* ================= PRIMARY ================= */
|
|
44
|
+
function parsePrimary() {
|
|
45
|
+
const token = consume();
|
|
46
|
+
if (!token) throw new Error("Unexpected end of input");
|
|
47
|
+
|
|
48
|
+
switch (token.type) {
|
|
49
|
+
case "Number":
|
|
50
|
+
case "BigInt":
|
|
51
|
+
case "Boolean":
|
|
52
|
+
case "String":
|
|
53
|
+
return { type: "Literal", value: token.value };
|
|
54
|
+
|
|
55
|
+
case "ImaginaryLiteral":
|
|
56
|
+
return { type: "ImaginaryLiteral", value: token.value };
|
|
57
|
+
|
|
58
|
+
case "NumberWithUnit":
|
|
59
|
+
return {
|
|
60
|
+
type: "UnitLiteral",
|
|
61
|
+
value: token.value,
|
|
62
|
+
unit: token.unit
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
case "Identifier":
|
|
66
|
+
return { type: "Identifier", name: token.name };
|
|
67
|
+
|
|
68
|
+
case "Function": // 🔥 ADD THIS
|
|
69
|
+
return {
|
|
70
|
+
type: "Identifier",
|
|
71
|
+
name: token.name
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
case "Parenthesis":
|
|
75
|
+
if (token.value === "(") {
|
|
76
|
+
const expr = parseExpression();
|
|
77
|
+
|
|
78
|
+
if (!match("Parenthesis", ")")) {
|
|
79
|
+
throw new Error(`Expected ')'`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return expr;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case "ArrayStart": {
|
|
86
|
+
const rows = [];
|
|
87
|
+
let currentRow = [];
|
|
88
|
+
|
|
89
|
+
if (!match("ArrayEnd")) {
|
|
90
|
+
while (true) {
|
|
91
|
+
currentRow.push(parseExpression());
|
|
92
|
+
|
|
93
|
+
if (match("Comma")) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (match("Semicolon")) {
|
|
98
|
+
rows.push(currentRow);
|
|
99
|
+
currentRow = [];
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (match("ArrayEnd")) {
|
|
104
|
+
rows.push(currentRow);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error(`Expected ',', ';', or ']' at ${current}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!rows.length) {
|
|
113
|
+
return { type: "ArrayExpression", elements: [] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (rows.length === 1) {
|
|
117
|
+
return { type: "ArrayExpression", elements: rows[0] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
type: "ArrayExpression",
|
|
122
|
+
elements: rows.map((elements) => ({
|
|
123
|
+
type: "ArrayExpression",
|
|
124
|
+
elements
|
|
125
|
+
}))
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case "BlockStart": {
|
|
130
|
+
const properties = [];
|
|
131
|
+
|
|
132
|
+
if (!match("BlockEnd")) {
|
|
133
|
+
do {
|
|
134
|
+
const keyToken = consume();
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
keyToken.type !== "Identifier" &&
|
|
138
|
+
keyToken.type !== "String"
|
|
139
|
+
) {
|
|
140
|
+
throw new Error("Invalid object key");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!match("Colon")) {
|
|
144
|
+
throw new Error("Expected ':' after key");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const value = parseExpression();
|
|
148
|
+
|
|
149
|
+
properties.push({
|
|
150
|
+
key: keyToken.value,
|
|
151
|
+
value
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
} while (match("Comma"));
|
|
155
|
+
|
|
156
|
+
if (!match("BlockEnd")) {
|
|
157
|
+
throw new Error(`Expected '}' at ${current}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { type: "ObjectExpression", properties };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new Error(`Unexpected token: ${JSON.stringify(token)}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ================= MEMBER ================= */
|
|
169
|
+
function parseMember() {
|
|
170
|
+
let object = parsePrimary();
|
|
171
|
+
|
|
172
|
+
while (true) {
|
|
173
|
+
if (match("ArrayStart")) {
|
|
174
|
+
const selectors = [];
|
|
175
|
+
|
|
176
|
+
if (!match("ArrayEnd")) {
|
|
177
|
+
do {
|
|
178
|
+
selectors.push(parseSliceOrIndex());
|
|
179
|
+
} while (match("Comma"));
|
|
180
|
+
|
|
181
|
+
if (!match("ArrayEnd")) {
|
|
182
|
+
throw new Error(`Expected ']' at ${current}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
object = {
|
|
187
|
+
type: "IndexExpression",
|
|
188
|
+
object,
|
|
189
|
+
selectors
|
|
190
|
+
};
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (match("Dot")) {
|
|
195
|
+
const property = consume();
|
|
196
|
+
|
|
197
|
+
if (property.type !== "Identifier") {
|
|
198
|
+
throw new Error("Expected property after '.'");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
object = {
|
|
202
|
+
type: "MemberExpression",
|
|
203
|
+
object,
|
|
204
|
+
property: { type: "Identifier", name: property.value },
|
|
205
|
+
optional: false
|
|
206
|
+
};
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (match("Operator", "?.")) {
|
|
211
|
+
const property = consume();
|
|
212
|
+
|
|
213
|
+
object = {
|
|
214
|
+
type: "MemberExpression",
|
|
215
|
+
object,
|
|
216
|
+
property: { type: "Identifier", name: property.value },
|
|
217
|
+
optional: true
|
|
218
|
+
};
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return object;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ================= CALL ================= */
|
|
229
|
+
function parseCallChain() {
|
|
230
|
+
let expr = parseMember();
|
|
231
|
+
|
|
232
|
+
while (peek()?.type === "Parenthesis" && peek()?.value === "(") {
|
|
233
|
+
consume(); // '('
|
|
234
|
+
|
|
235
|
+
const args = [];
|
|
236
|
+
|
|
237
|
+
if (!(peek()?.type === "Parenthesis" && peek()?.value === ")")) {
|
|
238
|
+
do {
|
|
239
|
+
args.push(parseExpression());
|
|
240
|
+
} while (match("Comma"));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!match("Parenthesis", ")")) {
|
|
244
|
+
throw new Error(`Expected ')' at ${current}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
expr = {
|
|
248
|
+
type: "CallExpression",
|
|
249
|
+
callee: expr,
|
|
250
|
+
arguments: args
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return expr;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* ================= UNARY ================= */
|
|
258
|
+
function parseUnary() {
|
|
259
|
+
if (match("UnaryOperator")) {
|
|
260
|
+
const operator = tokens[current - 1].value;
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
type: "UnaryExpression",
|
|
264
|
+
operator,
|
|
265
|
+
argument: parseUnary()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return parseCallChain();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* ================= POWER ================= */
|
|
273
|
+
function parsePower() {
|
|
274
|
+
let left = parseUnary();
|
|
275
|
+
|
|
276
|
+
if (match("Operator", "^")) {
|
|
277
|
+
const right = parsePower();
|
|
278
|
+
return {
|
|
279
|
+
type: "BinaryExpression",
|
|
280
|
+
operator: "^",
|
|
281
|
+
left,
|
|
282
|
+
right
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return left;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ================= MULT ================= */
|
|
290
|
+
function parseMultiplication() {
|
|
291
|
+
let left = parsePower();
|
|
292
|
+
|
|
293
|
+
while (
|
|
294
|
+
match("Operator", "*") ||
|
|
295
|
+
match("Operator", "/") ||
|
|
296
|
+
match("Operator", "%")
|
|
297
|
+
) {
|
|
298
|
+
const operator = tokens[current - 1].value;
|
|
299
|
+
const right = parsePower();
|
|
300
|
+
|
|
301
|
+
left = {
|
|
302
|
+
type: "BinaryExpression",
|
|
303
|
+
operator,
|
|
304
|
+
left,
|
|
305
|
+
right
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return left;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* ================= ADD ================= */
|
|
313
|
+
function parseAddition() {
|
|
314
|
+
let left = parseMultiplication();
|
|
315
|
+
|
|
316
|
+
while (match("Operator", "+") || match("Operator", "-")) {
|
|
317
|
+
const operator = tokens[current - 1].value;
|
|
318
|
+
const right = parseMultiplication();
|
|
319
|
+
|
|
320
|
+
left = {
|
|
321
|
+
type: "BinaryExpression",
|
|
322
|
+
operator,
|
|
323
|
+
left,
|
|
324
|
+
right
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return left;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* ================= UNIT CONVERSION ================= */
|
|
332
|
+
function parseUnitConversion() {
|
|
333
|
+
let left = parseAddition();
|
|
334
|
+
|
|
335
|
+
const nextKeyword = peek();
|
|
336
|
+
if (nextKeyword?.type === "Keyword" && ["to", "in"].includes(nextKeyword.value)) {
|
|
337
|
+
consume();
|
|
338
|
+
const next = consume();
|
|
339
|
+
|
|
340
|
+
if (!next || next.type !== "Unit") {
|
|
341
|
+
throw new Error(`Expected unit after '${nextKeyword.value}'`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
type: "UnitConversion",
|
|
346
|
+
from: left,
|
|
347
|
+
to: next.value
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return left;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* ================= COMPARISON ================= */
|
|
355
|
+
function parseComparison() {
|
|
356
|
+
let left = parseUnitConversion();
|
|
357
|
+
|
|
358
|
+
while (
|
|
359
|
+
match("Operator", ">") ||
|
|
360
|
+
match("Operator", "<") ||
|
|
361
|
+
match("Operator", ">=") ||
|
|
362
|
+
match("Operator", "<=") ||
|
|
363
|
+
match("Operator", "==")
|
|
364
|
+
) {
|
|
365
|
+
const operator = tokens[current - 1].value;
|
|
366
|
+
const right = parseUnitConversion();
|
|
367
|
+
|
|
368
|
+
left = {
|
|
369
|
+
type: "BinaryExpression",
|
|
370
|
+
operator,
|
|
371
|
+
left,
|
|
372
|
+
right
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return left;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* ================= LOGICAL ================= */
|
|
380
|
+
function parseLogical() {
|
|
381
|
+
let left = parseComparison();
|
|
382
|
+
|
|
383
|
+
while (
|
|
384
|
+
match("Operator", "&&") ||
|
|
385
|
+
match("Operator", "||")
|
|
386
|
+
) {
|
|
387
|
+
const operator = tokens[current - 1].value;
|
|
388
|
+
const right = parseComparison();
|
|
389
|
+
|
|
390
|
+
left = {
|
|
391
|
+
type: "LogicalExpression",
|
|
392
|
+
operator,
|
|
393
|
+
left,
|
|
394
|
+
right
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return left;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* ================= NULLISH ================= */
|
|
402
|
+
function parseNullish() {
|
|
403
|
+
let left = parseLogical();
|
|
404
|
+
|
|
405
|
+
while (match("Operator", "??")) {
|
|
406
|
+
const right = parseLogical();
|
|
407
|
+
|
|
408
|
+
left = {
|
|
409
|
+
type: "LogicalExpression",
|
|
410
|
+
operator: "??",
|
|
411
|
+
left,
|
|
412
|
+
right
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return left;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* ================= TERNARY ================= */
|
|
420
|
+
function parseTernary() {
|
|
421
|
+
let test = parseNullish();
|
|
422
|
+
|
|
423
|
+
if (match("Ternary", "?")) {
|
|
424
|
+
const consequent = parseExpression();
|
|
425
|
+
|
|
426
|
+
if (!match("Ternary", ":")) {
|
|
427
|
+
throw new Error("Expected ':' in ternary");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const alternate = parseExpression();
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
type: "ConditionalExpression",
|
|
434
|
+
test,
|
|
435
|
+
consequent,
|
|
436
|
+
alternate
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return test;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* ================= PIPELINE ================= */
|
|
444
|
+
function parsePipeline() {
|
|
445
|
+
let left = parseTernary();
|
|
446
|
+
|
|
447
|
+
while (match("Operator", "|>")) {
|
|
448
|
+
const right = parseTernary();
|
|
449
|
+
|
|
450
|
+
left = {
|
|
451
|
+
type: "PipelineExpression",
|
|
452
|
+
left,
|
|
453
|
+
right
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return left;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* ================= ASSIGNMENT ================= */
|
|
461
|
+
function parseAssignment() {
|
|
462
|
+
let left = parsePipeline();
|
|
463
|
+
|
|
464
|
+
if (
|
|
465
|
+
match("Operator", "=") ||
|
|
466
|
+
match("Operator", "+=") ||
|
|
467
|
+
match("Operator", "-=") ||
|
|
468
|
+
match("Operator", "*=") ||
|
|
469
|
+
match("Operator", "/=")
|
|
470
|
+
) {
|
|
471
|
+
const operator = tokens[current - 1].value;
|
|
472
|
+
|
|
473
|
+
if (
|
|
474
|
+
left.type !== "Identifier" &&
|
|
475
|
+
left.type !== "MemberExpression" &&
|
|
476
|
+
left.type !== "IndexExpression"
|
|
477
|
+
) {
|
|
478
|
+
throw new Error("Invalid assignment target");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const right = parseAssignment();
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
type: "AssignmentExpression",
|
|
485
|
+
operator,
|
|
486
|
+
left,
|
|
487
|
+
right
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return left;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* ================= ENTRY ================= */
|
|
495
|
+
function parseExpression() {
|
|
496
|
+
return parseAssignment();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const ast = parseExpression();
|
|
500
|
+
|
|
501
|
+
if (current < tokens.length) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
`Unexpected token at end: ${JSON.stringify(peek())}`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return ast;
|
|
508
|
+
}
|