starlight-cli 1.1.11 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -357
- package/dist/index.js +1553 -529
- package/package.json +1 -1
- package/src/evaluator.js +649 -363
- package/src/parser.js +904 -166
- package/src/starlight.js +1 -1
package/src/parser.js
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
|
+
const COLOR = {
|
|
2
|
+
reset: '\x1b[0m',
|
|
3
|
+
red: '\x1b[31m',
|
|
4
|
+
yellow: '\x1b[33m',
|
|
5
|
+
white: '\x1b[37m'
|
|
6
|
+
};
|
|
7
|
+
|
|
1
8
|
class ParseError extends Error {
|
|
2
|
-
constructor(message, token, source) {
|
|
9
|
+
constructor(message, token, source, suggestion = null) {
|
|
3
10
|
const line = token?.line ?? '?';
|
|
4
11
|
const column = token?.column ?? '?';
|
|
5
12
|
|
|
6
|
-
let output = `${message}\n`;
|
|
13
|
+
let output = `${COLOR.red}${message}${COLOR.reset}\n`;
|
|
14
|
+
|
|
15
|
+
if (suggestion) {
|
|
16
|
+
output += `${COLOR.yellow}${suggestion}${COLOR.reset}\n`;
|
|
17
|
+
}
|
|
7
18
|
|
|
8
19
|
if (source && token?.line != null) {
|
|
9
20
|
const lines = source.split('\n');
|
|
10
21
|
const srcLine = lines[token.line - 1] || '';
|
|
11
|
-
|
|
12
|
-
output +=
|
|
13
|
-
|
|
22
|
+
|
|
23
|
+
output += `${COLOR.white} at line ${line}, column ${column}\n`;
|
|
24
|
+
const caretPos =
|
|
25
|
+
typeof column === 'number' && column > 0
|
|
26
|
+
? column - 1
|
|
27
|
+
: 0;
|
|
28
|
+
|
|
29
|
+
output += ` ${srcLine}\n`;
|
|
30
|
+
output += ` ${' '.repeat(caretPos)}^\n${COLOR.reset}`;
|
|
31
|
+
|
|
14
32
|
} else {
|
|
15
|
-
output +=
|
|
33
|
+
output += `${COLOR.white} at line ${line}, column ${column}\n${COLOR.reset}`;
|
|
16
34
|
}
|
|
17
35
|
|
|
18
36
|
super(output);
|
|
@@ -87,17 +105,49 @@ class Parser {
|
|
|
87
105
|
varDeclaration() {
|
|
88
106
|
const t = this.current;
|
|
89
107
|
this.eat('LET');
|
|
108
|
+
|
|
109
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
110
|
+
throw new ParseError(
|
|
111
|
+
"Expected variable name after 'let'",
|
|
112
|
+
this.current,
|
|
113
|
+
this.source,
|
|
114
|
+
"Variable declarations must be followed by an identifier, e.g. let x = 5"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
const idToken = this.current;
|
|
91
119
|
const id = idToken.value;
|
|
92
120
|
this.eat('IDENTIFIER');
|
|
93
121
|
|
|
94
122
|
let expr = null;
|
|
123
|
+
|
|
124
|
+
if (this.current.type === 'EQEQ') {
|
|
125
|
+
throw new ParseError(
|
|
126
|
+
"Invalid '==' in variable declaration",
|
|
127
|
+
this.current,
|
|
128
|
+
this.source,
|
|
129
|
+
"Did you mean '=' for assignment?"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
95
133
|
if (this.current.type === 'EQUAL') {
|
|
96
134
|
this.eat('EQUAL');
|
|
135
|
+
|
|
136
|
+
if (this.current.type === 'SEMICOLON') {
|
|
137
|
+
throw new ParseError(
|
|
138
|
+
"Expected expression after '='",
|
|
139
|
+
this.current,
|
|
140
|
+
this.source,
|
|
141
|
+
"Assignments require a value, e.g. let x = 10"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
97
145
|
expr = this.expression();
|
|
98
146
|
}
|
|
99
147
|
|
|
100
|
-
if (this.current.type === 'SEMICOLON')
|
|
148
|
+
if (this.current.type === 'SEMICOLON') {
|
|
149
|
+
this.eat('SEMICOLON');
|
|
150
|
+
}
|
|
101
151
|
|
|
102
152
|
return {
|
|
103
153
|
type: 'VarDeclaration',
|
|
@@ -107,12 +157,31 @@ varDeclaration() {
|
|
|
107
157
|
column: t.column
|
|
108
158
|
};
|
|
109
159
|
}
|
|
160
|
+
|
|
110
161
|
startStatement() {
|
|
111
162
|
const t = this.current;
|
|
112
163
|
this.eat('START');
|
|
113
164
|
|
|
165
|
+
if (this.current.type === 'LBRACE') {
|
|
166
|
+
throw new ParseError(
|
|
167
|
+
"Expected expression after 'start'",
|
|
168
|
+
this.current,
|
|
169
|
+
this.source,
|
|
170
|
+
"The 'start' statement requires a discriminant expression"
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
114
174
|
const discriminant = this.expression();
|
|
115
175
|
|
|
176
|
+
if (this.current.type !== 'LBRACE') {
|
|
177
|
+
throw new ParseError(
|
|
178
|
+
"Expected '{' to start start-block",
|
|
179
|
+
this.current,
|
|
180
|
+
this.source,
|
|
181
|
+
"Start blocks must be enclosed in braces"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
116
185
|
this.eat('LBRACE');
|
|
117
186
|
|
|
118
187
|
const cases = [];
|
|
@@ -121,6 +190,24 @@ startStatement() {
|
|
|
121
190
|
cases.push(this.raceClause());
|
|
122
191
|
}
|
|
123
192
|
|
|
193
|
+
if (cases.length === 0) {
|
|
194
|
+
throw new ParseError(
|
|
195
|
+
"Start statement must contain at least one 'race' clause",
|
|
196
|
+
this.current,
|
|
197
|
+
this.source,
|
|
198
|
+
"Use 'race <condition> { ... }' inside start blocks"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (this.current.type !== 'RBRACE') {
|
|
203
|
+
throw new ParseError(
|
|
204
|
+
"Expected '}' to close start block",
|
|
205
|
+
this.current,
|
|
206
|
+
this.source,
|
|
207
|
+
"Did you forget to close the start block?"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
124
211
|
this.eat('RBRACE');
|
|
125
212
|
|
|
126
213
|
return {
|
|
@@ -135,8 +222,26 @@ raceClause() {
|
|
|
135
222
|
const t = this.current;
|
|
136
223
|
this.eat('RACE');
|
|
137
224
|
|
|
225
|
+
if (this.current.type === 'LBRACE') {
|
|
226
|
+
throw new ParseError(
|
|
227
|
+
"Expected condition after 'race'",
|
|
228
|
+
this.current,
|
|
229
|
+
this.source,
|
|
230
|
+
"Race clauses require a condition before the block"
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
138
234
|
const test = this.expression();
|
|
139
235
|
|
|
236
|
+
if (this.current.type !== 'LBRACE') {
|
|
237
|
+
throw new ParseError(
|
|
238
|
+
"Expected '{' after race condition",
|
|
239
|
+
this.current,
|
|
240
|
+
this.source,
|
|
241
|
+
"Race clauses must use a block: race condition { ... }"
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
140
245
|
const consequent = this.block();
|
|
141
246
|
|
|
142
247
|
return {
|
|
@@ -148,11 +253,26 @@ raceClause() {
|
|
|
148
253
|
};
|
|
149
254
|
}
|
|
150
255
|
|
|
256
|
+
|
|
151
257
|
sldeployStatement() {
|
|
152
258
|
const t = this.current;
|
|
153
259
|
this.eat('SLDEPLOY');
|
|
260
|
+
|
|
261
|
+
if (this.current.type === 'SEMICOLON') {
|
|
262
|
+
throw new ParseError(
|
|
263
|
+
"Expected expression after 'sldeploy'",
|
|
264
|
+
this.current,
|
|
265
|
+
this.source,
|
|
266
|
+
"sldeploy requires a value or expression to deploy"
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
154
270
|
const expr = this.expression();
|
|
155
|
-
|
|
271
|
+
|
|
272
|
+
if (this.current.type === 'SEMICOLON') {
|
|
273
|
+
this.eat('SEMICOLON');
|
|
274
|
+
}
|
|
275
|
+
|
|
156
276
|
return {
|
|
157
277
|
type: 'SldeployStatement',
|
|
158
278
|
expr,
|
|
@@ -165,11 +285,31 @@ doTrackStatement() {
|
|
|
165
285
|
const t = this.current;
|
|
166
286
|
this.eat('DO');
|
|
167
287
|
|
|
288
|
+
if (this.current.type !== 'LBRACE') {
|
|
289
|
+
throw new ParseError(
|
|
290
|
+
"Expected '{' after 'do'",
|
|
291
|
+
this.current,
|
|
292
|
+
this.source,
|
|
293
|
+
"The 'do' statement must be followed by a block"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
168
297
|
const body = this.block();
|
|
169
298
|
|
|
170
299
|
let handler = null;
|
|
300
|
+
|
|
171
301
|
if (this.current.type === 'TRACK') {
|
|
172
302
|
this.eat('TRACK');
|
|
303
|
+
|
|
304
|
+
if (this.current.type !== 'LBRACE') {
|
|
305
|
+
throw new ParseError(
|
|
306
|
+
"Expected '{' after 'track'",
|
|
307
|
+
this.current,
|
|
308
|
+
this.source,
|
|
309
|
+
"Track handlers must be blocks"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
173
313
|
handler = this.block();
|
|
174
314
|
}
|
|
175
315
|
|
|
@@ -185,17 +325,49 @@ doTrackStatement() {
|
|
|
185
325
|
defineStatement() {
|
|
186
326
|
const t = this.current;
|
|
187
327
|
this.eat('DEFINE');
|
|
328
|
+
|
|
329
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
330
|
+
throw new ParseError(
|
|
331
|
+
"Expected identifier after 'define'",
|
|
332
|
+
this.current,
|
|
333
|
+
this.source,
|
|
334
|
+
"Definitions must be followed by a name, e.g. define PI = 3.14"
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
188
338
|
const idToken = this.current;
|
|
189
339
|
const id = idToken.value;
|
|
190
340
|
this.eat('IDENTIFIER');
|
|
191
341
|
|
|
192
342
|
let expr = null;
|
|
343
|
+
|
|
344
|
+
if (this.current.type === 'EQEQ') {
|
|
345
|
+
throw new ParseError(
|
|
346
|
+
"Invalid '==' in define statement",
|
|
347
|
+
this.current,
|
|
348
|
+
this.source,
|
|
349
|
+
"Did you mean '=' to assign a value?"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
193
353
|
if (this.current.type === 'EQUAL') {
|
|
194
354
|
this.eat('EQUAL');
|
|
355
|
+
|
|
356
|
+
if (this.current.type === 'SEMICOLON') {
|
|
357
|
+
throw new ParseError(
|
|
358
|
+
"Expected expression after '='",
|
|
359
|
+
this.current,
|
|
360
|
+
this.source,
|
|
361
|
+
"Definitions require a value, e.g. define X = 10"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
195
365
|
expr = this.expression();
|
|
196
366
|
}
|
|
197
367
|
|
|
198
|
-
if (this.current.type === 'SEMICOLON')
|
|
368
|
+
if (this.current.type === 'SEMICOLON') {
|
|
369
|
+
this.eat('SEMICOLON');
|
|
370
|
+
}
|
|
199
371
|
|
|
200
372
|
return {
|
|
201
373
|
type: 'DefineStatement',
|
|
@@ -208,30 +380,80 @@ defineStatement() {
|
|
|
208
380
|
|
|
209
381
|
asyncFuncDeclaration() {
|
|
210
382
|
const t = this.current;
|
|
383
|
+
|
|
384
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
385
|
+
throw new ParseError(
|
|
386
|
+
"Expected function name after 'async func'",
|
|
387
|
+
this.current,
|
|
388
|
+
this.source,
|
|
389
|
+
"Async functions must have a name"
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
211
393
|
const name = this.current.value;
|
|
212
394
|
this.eat('IDENTIFIER');
|
|
213
395
|
|
|
214
396
|
let params = [];
|
|
397
|
+
|
|
215
398
|
if (this.current.type === 'LPAREN') {
|
|
216
399
|
this.eat('LPAREN');
|
|
400
|
+
|
|
217
401
|
if (this.current.type !== 'RPAREN') {
|
|
402
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
403
|
+
throw new ParseError(
|
|
404
|
+
"Expected parameter name",
|
|
405
|
+
this.current,
|
|
406
|
+
this.source,
|
|
407
|
+
"Function parameters must be identifiers"
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
218
411
|
params.push(this.current.value);
|
|
219
412
|
this.eat('IDENTIFIER');
|
|
413
|
+
|
|
220
414
|
while (this.current.type === 'COMMA') {
|
|
221
415
|
this.eat('COMMA');
|
|
416
|
+
|
|
417
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
418
|
+
throw new ParseError(
|
|
419
|
+
"Expected parameter name after ','",
|
|
420
|
+
this.current,
|
|
421
|
+
this.source,
|
|
422
|
+
"Separate parameters with commas"
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
222
426
|
params.push(this.current.value);
|
|
223
427
|
this.eat('IDENTIFIER');
|
|
224
428
|
}
|
|
225
429
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
430
|
+
|
|
431
|
+
if (this.current.type !== 'RPAREN') {
|
|
432
|
+
throw new ParseError(
|
|
433
|
+
"Expected ')' after function parameters",
|
|
434
|
+
this.current,
|
|
435
|
+
this.source,
|
|
436
|
+
"Did you forget to close the parameter list?"
|
|
437
|
+
);
|
|
231
438
|
}
|
|
439
|
+
|
|
440
|
+
this.eat('RPAREN');
|
|
441
|
+
} else if (this.current.type === 'IDENTIFIER') {
|
|
442
|
+
params.push(this.current.value);
|
|
443
|
+
this.eat('IDENTIFIER');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (this.current.type !== 'LBRACE') {
|
|
447
|
+
throw new ParseError(
|
|
448
|
+
"Expected '{' to start function body",
|
|
449
|
+
this.current,
|
|
450
|
+
this.source,
|
|
451
|
+
"Function declarations require a block body"
|
|
452
|
+
);
|
|
232
453
|
}
|
|
233
454
|
|
|
234
455
|
const body = this.block();
|
|
456
|
+
|
|
235
457
|
return {
|
|
236
458
|
type: 'FunctionDeclaration',
|
|
237
459
|
name,
|
|
@@ -242,32 +464,62 @@ asyncFuncDeclaration() {
|
|
|
242
464
|
column: t.column
|
|
243
465
|
};
|
|
244
466
|
}
|
|
245
|
-
|
|
246
467
|
ifStatement() {
|
|
247
468
|
const t = this.current;
|
|
248
469
|
this.eat('IF');
|
|
249
470
|
|
|
250
471
|
let test;
|
|
472
|
+
|
|
251
473
|
if (this.current.type === 'LPAREN') {
|
|
252
474
|
this.eat('LPAREN');
|
|
475
|
+
|
|
476
|
+
if (this.current.type === 'RPAREN') {
|
|
477
|
+
throw new ParseError(
|
|
478
|
+
"Missing condition in if statement",
|
|
479
|
+
this.current,
|
|
480
|
+
this.source,
|
|
481
|
+
"If statements require a condition"
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
253
485
|
test = this.expression();
|
|
486
|
+
|
|
487
|
+
if (this.current.type !== 'RPAREN') {
|
|
488
|
+
throw new ParseError(
|
|
489
|
+
"Expected ')' after if condition",
|
|
490
|
+
this.current,
|
|
491
|
+
this.source,
|
|
492
|
+
"Did you forget to close the condition?"
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
254
496
|
this.eat('RPAREN');
|
|
255
497
|
} else {
|
|
256
498
|
test = this.expression();
|
|
257
499
|
}
|
|
258
500
|
|
|
501
|
+
if (this.current.type === 'EQUAL') {
|
|
502
|
+
throw new ParseError(
|
|
503
|
+
"Invalid assignment in if condition",
|
|
504
|
+
this.current,
|
|
505
|
+
this.source,
|
|
506
|
+
"Did you mean '==' for comparison?"
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
259
510
|
const consequent = this.statementOrBlock();
|
|
260
511
|
|
|
261
512
|
let alternate = null;
|
|
513
|
+
|
|
262
514
|
if (this.current.type === 'ELSE') {
|
|
263
|
-
|
|
264
|
-
if (this.current.type === 'IF') {
|
|
265
|
-
alternate = this.ifStatement();
|
|
266
|
-
} else {
|
|
267
|
-
alternate = this.statementOrBlock();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
515
|
+
this.eat('ELSE');
|
|
270
516
|
|
|
517
|
+
if (this.current.type === 'IF') {
|
|
518
|
+
alternate = this.ifStatement();
|
|
519
|
+
} else {
|
|
520
|
+
alternate = this.statementOrBlock();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
271
523
|
|
|
272
524
|
return {
|
|
273
525
|
type: 'IfStatement',
|
|
@@ -278,6 +530,7 @@ ifStatement() {
|
|
|
278
530
|
column: t.column
|
|
279
531
|
};
|
|
280
532
|
}
|
|
533
|
+
|
|
281
534
|
parseExpressionOnly() {
|
|
282
535
|
return this.expression();
|
|
283
536
|
}
|
|
@@ -290,13 +543,30 @@ whileStatement() {
|
|
|
290
543
|
|
|
291
544
|
if (this.current.type === 'LPAREN') {
|
|
292
545
|
this.eat('LPAREN');
|
|
546
|
+
|
|
547
|
+
if (this.current.type === 'RPAREN') {
|
|
548
|
+
throw new ParseError(
|
|
549
|
+
"Missing condition in while statement",
|
|
550
|
+
this.current,
|
|
551
|
+
this.source,
|
|
552
|
+
"While loops require a condition"
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
293
556
|
test = this.expression();
|
|
557
|
+
|
|
558
|
+
if (this.current.type !== 'RPAREN') {
|
|
559
|
+
throw new ParseError(
|
|
560
|
+
"Expected ')' after while condition",
|
|
561
|
+
this.current,
|
|
562
|
+
this.source,
|
|
563
|
+
"Did you forget to close the condition?"
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
294
567
|
this.eat('RPAREN');
|
|
295
568
|
}
|
|
296
569
|
else {
|
|
297
|
-
const startPos = this.pos;
|
|
298
|
-
const startToken = this.current;
|
|
299
|
-
|
|
300
570
|
const exprTokens = [];
|
|
301
571
|
let braceFound = false;
|
|
302
572
|
let depth = 0;
|
|
@@ -316,7 +586,8 @@ whileStatement() {
|
|
|
316
586
|
throw new ParseError(
|
|
317
587
|
"Expected '{' after while condition",
|
|
318
588
|
this.current,
|
|
319
|
-
this.source
|
|
589
|
+
this.source,
|
|
590
|
+
"While loops must be followed by a block"
|
|
320
591
|
);
|
|
321
592
|
}
|
|
322
593
|
|
|
@@ -325,7 +596,17 @@ whileStatement() {
|
|
|
325
596
|
test = exprParser.parseExpressionOnly();
|
|
326
597
|
}
|
|
327
598
|
|
|
599
|
+
if (this.current.type !== 'LBRACE') {
|
|
600
|
+
throw new ParseError(
|
|
601
|
+
"Expected '{' to start while loop body",
|
|
602
|
+
this.current,
|
|
603
|
+
this.source,
|
|
604
|
+
"While loop bodies must be enclosed in braces"
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
328
608
|
const body = this.block();
|
|
609
|
+
|
|
329
610
|
return {
|
|
330
611
|
type: 'WhileStatement',
|
|
331
612
|
test,
|
|
@@ -335,21 +616,67 @@ whileStatement() {
|
|
|
335
616
|
};
|
|
336
617
|
}
|
|
337
618
|
|
|
338
|
-
|
|
339
619
|
importStatement() {
|
|
340
620
|
const t = this.current;
|
|
341
621
|
this.eat('IMPORT');
|
|
342
622
|
|
|
343
623
|
let specifiers = [];
|
|
624
|
+
|
|
344
625
|
if (this.current.type === 'STAR') {
|
|
345
626
|
this.eat('STAR');
|
|
627
|
+
|
|
628
|
+
if (this.current.type !== 'AS') {
|
|
629
|
+
throw new ParseError(
|
|
630
|
+
"Expected 'as' after '*' in import",
|
|
631
|
+
this.current,
|
|
632
|
+
this.source,
|
|
633
|
+
"Namespace imports require 'as', e.g. import * as name from 'mod'"
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
346
637
|
this.eat('AS');
|
|
638
|
+
|
|
639
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
640
|
+
throw new ParseError(
|
|
641
|
+
"Expected identifier after 'as'",
|
|
642
|
+
this.current,
|
|
643
|
+
this.source,
|
|
644
|
+
"The namespace must have a local name"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
347
648
|
const name = this.current.value;
|
|
348
649
|
this.eat('IDENTIFIER');
|
|
349
|
-
|
|
650
|
+
|
|
651
|
+
specifiers.push({
|
|
652
|
+
type: 'NamespaceImport',
|
|
653
|
+
local: name,
|
|
654
|
+
line: t.line,
|
|
655
|
+
column: t.column
|
|
656
|
+
});
|
|
657
|
+
|
|
350
658
|
} else if (this.current.type === 'LBRACE') {
|
|
351
659
|
this.eat('LBRACE');
|
|
660
|
+
|
|
661
|
+
if (this.current.type === 'RBRACE') {
|
|
662
|
+
throw new ParseError(
|
|
663
|
+
"Empty import specifier list",
|
|
664
|
+
this.current,
|
|
665
|
+
this.source,
|
|
666
|
+
"Specify at least one name inside '{ }'"
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
352
670
|
while (this.current.type !== 'RBRACE') {
|
|
671
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
672
|
+
throw new ParseError(
|
|
673
|
+
"Expected identifier in import specifier",
|
|
674
|
+
this.current,
|
|
675
|
+
this.source,
|
|
676
|
+
"Import names must be identifiers"
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
353
680
|
const importedName = this.current.value;
|
|
354
681
|
const importedLine = this.current.line;
|
|
355
682
|
const importedColumn = this.current.column;
|
|
@@ -358,27 +685,77 @@ importStatement() {
|
|
|
358
685
|
let localName = importedName;
|
|
359
686
|
if (this.current.type === 'AS') {
|
|
360
687
|
this.eat('AS');
|
|
688
|
+
|
|
689
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
690
|
+
throw new ParseError(
|
|
691
|
+
"Expected identifier after 'as'",
|
|
692
|
+
this.current,
|
|
693
|
+
this.source,
|
|
694
|
+
"Aliases must be valid identifiers"
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
361
698
|
localName = this.current.value;
|
|
362
699
|
this.eat('IDENTIFIER');
|
|
363
700
|
}
|
|
364
701
|
|
|
365
|
-
specifiers.push({
|
|
702
|
+
specifiers.push({
|
|
703
|
+
type: 'NamedImport',
|
|
704
|
+
imported: importedName,
|
|
705
|
+
local: localName,
|
|
706
|
+
line: importedLine,
|
|
707
|
+
column: importedColumn
|
|
708
|
+
});
|
|
709
|
+
|
|
366
710
|
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
367
711
|
}
|
|
712
|
+
|
|
368
713
|
this.eat('RBRACE');
|
|
714
|
+
|
|
369
715
|
} else if (this.current.type === 'IDENTIFIER') {
|
|
370
716
|
const localName = this.current.value;
|
|
371
717
|
const localLine = this.current.line;
|
|
372
718
|
const localColumn = this.current.column;
|
|
373
719
|
this.eat('IDENTIFIER');
|
|
374
|
-
|
|
720
|
+
|
|
721
|
+
specifiers.push({
|
|
722
|
+
type: 'DefaultImport',
|
|
723
|
+
local: localName,
|
|
724
|
+
line: localLine,
|
|
725
|
+
column: localColumn
|
|
726
|
+
});
|
|
727
|
+
|
|
375
728
|
} else {
|
|
376
|
-
throw new
|
|
729
|
+
throw new ParseError(
|
|
730
|
+
"Invalid import syntax",
|
|
731
|
+
this.current,
|
|
732
|
+
this.source,
|
|
733
|
+
"Use import name, import { a }, or import * as name"
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (this.current.type !== 'FROM') {
|
|
738
|
+
throw new ParseError(
|
|
739
|
+
"Expected 'from' in import statement",
|
|
740
|
+
this.current,
|
|
741
|
+
this.source,
|
|
742
|
+
"Imports must specify a source module"
|
|
743
|
+
);
|
|
377
744
|
}
|
|
378
745
|
|
|
379
746
|
this.eat('FROM');
|
|
747
|
+
|
|
380
748
|
const pathToken = this.current;
|
|
381
|
-
|
|
749
|
+
|
|
750
|
+
if (pathToken.type !== 'STRING') {
|
|
751
|
+
throw new ParseError(
|
|
752
|
+
"Expected string after 'from'",
|
|
753
|
+
pathToken,
|
|
754
|
+
this.source,
|
|
755
|
+
"Module paths must be strings"
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
382
759
|
this.eat('STRING');
|
|
383
760
|
|
|
384
761
|
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
@@ -391,14 +768,14 @@ importStatement() {
|
|
|
391
768
|
column: t.column
|
|
392
769
|
};
|
|
393
770
|
}
|
|
394
|
-
|
|
395
771
|
forStatement() {
|
|
396
|
-
const t = this.current;
|
|
772
|
+
const t = this.current;
|
|
397
773
|
this.eat('FOR');
|
|
398
774
|
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
775
|
+
if (
|
|
776
|
+
(this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
|
|
777
|
+
(this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')
|
|
778
|
+
) {
|
|
402
779
|
let variable;
|
|
403
780
|
let variableLine, variableColumn;
|
|
404
781
|
let iterable;
|
|
@@ -407,22 +784,45 @@ forStatement() {
|
|
|
407
784
|
if (this.current.type === 'LET') {
|
|
408
785
|
letKeyword = true;
|
|
409
786
|
this.eat('LET');
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
790
|
+
throw new ParseError(
|
|
791
|
+
"Expected identifier in for-in loop",
|
|
792
|
+
this.current,
|
|
793
|
+
this.source,
|
|
794
|
+
"for-in loops require a loop variable"
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
variableLine = this.current.line;
|
|
799
|
+
variableColumn = this.current.column;
|
|
800
|
+
variable = this.current.value;
|
|
801
|
+
this.eat('IDENTIFIER');
|
|
802
|
+
|
|
803
|
+
if (this.current.type !== 'IN') {
|
|
804
|
+
throw new ParseError(
|
|
805
|
+
"Expected 'in' in for-in loop",
|
|
806
|
+
this.current,
|
|
807
|
+
this.source,
|
|
808
|
+
"Use: for item in iterable { ... }"
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
this.eat('IN');
|
|
813
|
+
iterable = this.expression();
|
|
814
|
+
|
|
815
|
+
if (this.current.type !== 'LBRACE') {
|
|
816
|
+
throw new ParseError(
|
|
817
|
+
"Expected '{' after for-in iterable",
|
|
818
|
+
this.current,
|
|
819
|
+
this.source,
|
|
820
|
+
"For-in loops require a block body"
|
|
821
|
+
);
|
|
423
822
|
}
|
|
424
823
|
|
|
425
824
|
const body = this.block();
|
|
825
|
+
|
|
426
826
|
return {
|
|
427
827
|
type: 'ForInStatement',
|
|
428
828
|
variable,
|
|
@@ -444,25 +844,53 @@ forStatement() {
|
|
|
444
844
|
this.eat('LPAREN');
|
|
445
845
|
|
|
446
846
|
if (this.current.type !== 'SEMICOLON') {
|
|
447
|
-
init = this.current.type === 'LET'
|
|
847
|
+
init = this.current.type === 'LET'
|
|
848
|
+
? this.varDeclaration()
|
|
849
|
+
: this.expressionStatement();
|
|
448
850
|
} else {
|
|
449
851
|
this.eat('SEMICOLON');
|
|
450
852
|
}
|
|
451
853
|
|
|
452
|
-
if (this.current.type !== 'SEMICOLON')
|
|
854
|
+
if (this.current.type !== 'SEMICOLON') {
|
|
855
|
+
test = this.expression();
|
|
856
|
+
}
|
|
857
|
+
|
|
453
858
|
this.eat('SEMICOLON');
|
|
454
859
|
|
|
455
|
-
if (this.current.type !== 'RPAREN')
|
|
860
|
+
if (this.current.type !== 'RPAREN') {
|
|
861
|
+
update = this.expression();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (this.current.type !== 'RPAREN') {
|
|
865
|
+
throw new ParseError(
|
|
866
|
+
"Expected ')' after for loop clauses",
|
|
867
|
+
this.current,
|
|
868
|
+
this.source,
|
|
869
|
+
"Did you forget to close the for loop header?"
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
456
873
|
this.eat('RPAREN');
|
|
457
874
|
} else {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
this.
|
|
461
|
-
|
|
462
|
-
|
|
875
|
+
throw new ParseError(
|
|
876
|
+
"Expected '(' after 'for'",
|
|
877
|
+
this.current,
|
|
878
|
+
this.source,
|
|
879
|
+
"Classic for loops require parentheses"
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (this.current.type !== 'LBRACE') {
|
|
884
|
+
throw new ParseError(
|
|
885
|
+
"Expected '{' to start for loop body",
|
|
886
|
+
this.current,
|
|
887
|
+
this.source,
|
|
888
|
+
"For loops require a block body"
|
|
889
|
+
);
|
|
463
890
|
}
|
|
464
891
|
|
|
465
892
|
const body = this.block();
|
|
893
|
+
|
|
466
894
|
return {
|
|
467
895
|
type: 'ForStatement',
|
|
468
896
|
init,
|
|
@@ -487,41 +915,111 @@ continueStatement() {
|
|
|
487
915
|
if (this.current.type === 'SEMICOLON') this.advance();
|
|
488
916
|
return { type: 'ContinueStatement', line: t.line, column: t.column };
|
|
489
917
|
}
|
|
490
|
-
|
|
491
918
|
funcDeclaration() {
|
|
492
919
|
const t = this.current;
|
|
493
920
|
this.eat('FUNC');
|
|
921
|
+
|
|
922
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
923
|
+
throw new ParseError(
|
|
924
|
+
"Expected function name after 'func'",
|
|
925
|
+
this.current,
|
|
926
|
+
this.source,
|
|
927
|
+
"Functions must have a name, e.g. func add(a, b) { ... }"
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
494
931
|
const nameToken = this.current;
|
|
495
932
|
const name = nameToken.value;
|
|
496
933
|
this.eat('IDENTIFIER');
|
|
497
934
|
|
|
498
935
|
let params = [];
|
|
936
|
+
|
|
499
937
|
if (this.current.type === 'LPAREN') {
|
|
500
938
|
this.eat('LPAREN');
|
|
939
|
+
|
|
501
940
|
if (this.current.type !== 'RPAREN') {
|
|
502
|
-
|
|
503
|
-
|
|
941
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
942
|
+
throw new ParseError(
|
|
943
|
+
"Expected parameter name",
|
|
944
|
+
this.current,
|
|
945
|
+
this.source,
|
|
946
|
+
"Function parameters must be identifiers"
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
let paramToken = this.current;
|
|
951
|
+
params.push({
|
|
952
|
+
name: paramToken.value,
|
|
953
|
+
line: paramToken.line,
|
|
954
|
+
column: paramToken.column
|
|
955
|
+
});
|
|
504
956
|
this.eat('IDENTIFIER');
|
|
957
|
+
|
|
505
958
|
while (this.current.type === 'COMMA') {
|
|
506
959
|
this.eat('COMMA');
|
|
507
|
-
|
|
508
|
-
|
|
960
|
+
|
|
961
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
962
|
+
throw new ParseError(
|
|
963
|
+
"Expected parameter name after ','",
|
|
964
|
+
this.current,
|
|
965
|
+
this.source,
|
|
966
|
+
"Each parameter must be an identifier"
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
paramToken = this.current;
|
|
971
|
+
params.push({
|
|
972
|
+
name: paramToken.value,
|
|
973
|
+
line: paramToken.line,
|
|
974
|
+
column: paramToken.column
|
|
975
|
+
});
|
|
509
976
|
this.eat('IDENTIFIER');
|
|
510
977
|
}
|
|
511
978
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
979
|
+
|
|
980
|
+
if (this.current.type !== 'RPAREN') {
|
|
981
|
+
throw new ParseError(
|
|
982
|
+
"Expected ')' after function parameters",
|
|
983
|
+
this.current,
|
|
984
|
+
this.source,
|
|
985
|
+
"Did you forget to close the parameter list?"
|
|
986
|
+
);
|
|
518
987
|
}
|
|
988
|
+
|
|
989
|
+
this.eat('RPAREN');
|
|
990
|
+
}
|
|
991
|
+
else if (this.current.type === 'IDENTIFIER') {
|
|
992
|
+
const paramToken = this.current;
|
|
993
|
+
params.push({
|
|
994
|
+
name: paramToken.value,
|
|
995
|
+
line: paramToken.line,
|
|
996
|
+
column: paramToken.column
|
|
997
|
+
});
|
|
998
|
+
this.eat('IDENTIFIER');
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (this.current.type !== 'LBRACE') {
|
|
1002
|
+
throw new ParseError(
|
|
1003
|
+
"Expected '{' to start function body",
|
|
1004
|
+
this.current,
|
|
1005
|
+
this.source,
|
|
1006
|
+
"Functions must have a block body"
|
|
1007
|
+
);
|
|
519
1008
|
}
|
|
520
1009
|
|
|
521
1010
|
const body = this.block();
|
|
522
|
-
|
|
1011
|
+
|
|
1012
|
+
return {
|
|
1013
|
+
type: 'FunctionDeclaration',
|
|
1014
|
+
name,
|
|
1015
|
+
params,
|
|
1016
|
+
body,
|
|
1017
|
+
line: t.line,
|
|
1018
|
+
column: t.column
|
|
1019
|
+
};
|
|
523
1020
|
}
|
|
524
1021
|
|
|
1022
|
+
|
|
525
1023
|
returnStatement() {
|
|
526
1024
|
const t = this.current; // RETURN token
|
|
527
1025
|
this.eat('RETURN');
|
|
@@ -541,18 +1039,35 @@ statementOrBlock() {
|
|
|
541
1039
|
}
|
|
542
1040
|
return this.statement();
|
|
543
1041
|
}
|
|
544
|
-
|
|
545
1042
|
block() {
|
|
546
1043
|
const t = this.current; // LBRACE token
|
|
547
1044
|
this.eat('LBRACE');
|
|
1045
|
+
|
|
548
1046
|
const body = [];
|
|
1047
|
+
|
|
549
1048
|
while (this.current.type !== 'RBRACE') {
|
|
1049
|
+
if (this.current.type === 'EOF') {
|
|
1050
|
+
throw new ParseError(
|
|
1051
|
+
"Unterminated block",
|
|
1052
|
+
this.current,
|
|
1053
|
+
this.source,
|
|
1054
|
+
"Did you forget to close the block with '}'?"
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
550
1057
|
body.push(this.statement());
|
|
551
1058
|
}
|
|
1059
|
+
|
|
552
1060
|
this.eat('RBRACE');
|
|
553
|
-
|
|
1061
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
type: 'BlockStatement',
|
|
1064
|
+
body,
|
|
1065
|
+
line: t.line,
|
|
1066
|
+
column: t.column
|
|
1067
|
+
};
|
|
554
1068
|
}
|
|
555
1069
|
|
|
1070
|
+
|
|
556
1071
|
expressionStatement() {
|
|
557
1072
|
const exprToken = this.current;
|
|
558
1073
|
const expr = this.expression();
|
|
@@ -563,7 +1078,6 @@ expressionStatement() {
|
|
|
563
1078
|
expression() {
|
|
564
1079
|
return this.assignment();
|
|
565
1080
|
}
|
|
566
|
-
|
|
567
1081
|
assignment() {
|
|
568
1082
|
const node = this.ternary();
|
|
569
1083
|
const compoundOps = ['PLUSEQ', 'MINUSEQ', 'STAREQ', 'SLASHEQ', 'MODEQ'];
|
|
@@ -574,29 +1088,76 @@ assignment() {
|
|
|
574
1088
|
const op = t.type;
|
|
575
1089
|
this.eat(op);
|
|
576
1090
|
const right = this.assignment();
|
|
577
|
-
|
|
1091
|
+
|
|
1092
|
+
return {
|
|
1093
|
+
type: 'CompoundAssignment',
|
|
1094
|
+
operator: op,
|
|
1095
|
+
left: node,
|
|
1096
|
+
right,
|
|
1097
|
+
line: t.line,
|
|
1098
|
+
column: t.column
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (t.type === 'EQEQ') {
|
|
1103
|
+
throw new ParseError(
|
|
1104
|
+
"Unexpected '==' in assignment",
|
|
1105
|
+
t,
|
|
1106
|
+
this.source,
|
|
1107
|
+
"Did you mean '=' to assign a value?"
|
|
1108
|
+
);
|
|
578
1109
|
}
|
|
579
1110
|
|
|
580
1111
|
if (t.type === 'EQUAL') {
|
|
581
1112
|
this.eat('EQUAL');
|
|
582
1113
|
const right = this.assignment();
|
|
583
|
-
|
|
1114
|
+
|
|
1115
|
+
return {
|
|
1116
|
+
type: 'AssignmentExpression',
|
|
1117
|
+
left: node,
|
|
1118
|
+
right,
|
|
1119
|
+
line: t.line,
|
|
1120
|
+
column: t.column
|
|
1121
|
+
};
|
|
584
1122
|
}
|
|
585
1123
|
|
|
586
1124
|
return node;
|
|
587
1125
|
}
|
|
1126
|
+
|
|
588
1127
|
ternary() {
|
|
589
1128
|
let node = this.nullishCoalescing();
|
|
1129
|
+
|
|
590
1130
|
while (this.current.type === 'QUESTION') {
|
|
591
1131
|
const t = this.current;
|
|
592
1132
|
this.eat('QUESTION');
|
|
593
|
-
|
|
1133
|
+
|
|
1134
|
+
const consequent = this.expression();
|
|
1135
|
+
|
|
1136
|
+
if (this.current.type !== 'COLON') {
|
|
1137
|
+
throw new ParseError(
|
|
1138
|
+
"Expected ':' in conditional expression",
|
|
1139
|
+
this.current,
|
|
1140
|
+
this.source,
|
|
1141
|
+
"Ternary expressions must follow the form: condition ? a : b"
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
594
1145
|
this.eat('COLON');
|
|
595
|
-
const alternate = this.expression();
|
|
596
|
-
|
|
1146
|
+
const alternate = this.expression();
|
|
1147
|
+
|
|
1148
|
+
node = {
|
|
1149
|
+
type: 'ConditionalExpression',
|
|
1150
|
+
test: node,
|
|
1151
|
+
consequent,
|
|
1152
|
+
alternate,
|
|
1153
|
+
line: t.line,
|
|
1154
|
+
column: t.column
|
|
1155
|
+
};
|
|
597
1156
|
}
|
|
1157
|
+
|
|
598
1158
|
return node;
|
|
599
1159
|
}
|
|
1160
|
+
|
|
600
1161
|
nullishCoalescing() {
|
|
601
1162
|
let node = this.logicalOr();
|
|
602
1163
|
while (this.current.type === 'NULLISH_COALESCING') {
|
|
@@ -679,6 +1240,16 @@ unary() {
|
|
|
679
1240
|
if (['NOT', 'MINUS', 'PLUS'].includes(t.type)) {
|
|
680
1241
|
const op = t.type;
|
|
681
1242
|
this.eat(op);
|
|
1243
|
+
|
|
1244
|
+
if (this.current.type === 'EOF') {
|
|
1245
|
+
throw new ParseError(
|
|
1246
|
+
"Missing operand for unary operator",
|
|
1247
|
+
this.current,
|
|
1248
|
+
this.source,
|
|
1249
|
+
"Unary operators must be followed by an expression"
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
682
1253
|
return {
|
|
683
1254
|
type: 'UnaryExpression',
|
|
684
1255
|
operator: op,
|
|
@@ -691,7 +1262,18 @@ unary() {
|
|
|
691
1262
|
if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
|
|
692
1263
|
const op = t.type;
|
|
693
1264
|
this.eat(op);
|
|
1265
|
+
|
|
694
1266
|
const argument = this.unary();
|
|
1267
|
+
|
|
1268
|
+
if (!argument || !argument.type) {
|
|
1269
|
+
throw new ParseError(
|
|
1270
|
+
"Invalid operand for update operator",
|
|
1271
|
+
t,
|
|
1272
|
+
this.source,
|
|
1273
|
+
"Increment and decrement operators must apply to a variable"
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
695
1277
|
return {
|
|
696
1278
|
type: 'UpdateExpression',
|
|
697
1279
|
operator: op,
|
|
@@ -704,9 +1286,9 @@ unary() {
|
|
|
704
1286
|
|
|
705
1287
|
return this.postfix();
|
|
706
1288
|
}
|
|
707
|
-
|
|
708
1289
|
postfix() {
|
|
709
1290
|
let node = this.primary();
|
|
1291
|
+
|
|
710
1292
|
while (true) {
|
|
711
1293
|
const t = this.current;
|
|
712
1294
|
|
|
@@ -714,9 +1296,27 @@ postfix() {
|
|
|
714
1296
|
const startLine = t.line;
|
|
715
1297
|
const startCol = t.column;
|
|
716
1298
|
this.eat('LBRACKET');
|
|
1299
|
+
|
|
717
1300
|
const index = this.expression();
|
|
718
|
-
|
|
719
|
-
|
|
1301
|
+
|
|
1302
|
+
if (this.current.type !== 'RBRACKET') {
|
|
1303
|
+
throw new ParseError(
|
|
1304
|
+
"Expected ']' after index expression",
|
|
1305
|
+
this.current,
|
|
1306
|
+
this.source,
|
|
1307
|
+
"Index access must be closed, e.g. arr[0]"
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
this.eat('RBRACKET');
|
|
1312
|
+
|
|
1313
|
+
node = {
|
|
1314
|
+
type: 'IndexExpression',
|
|
1315
|
+
object: node,
|
|
1316
|
+
indexer: index,
|
|
1317
|
+
line: startLine,
|
|
1318
|
+
column: startCol
|
|
1319
|
+
};
|
|
720
1320
|
continue;
|
|
721
1321
|
}
|
|
722
1322
|
|
|
@@ -724,13 +1324,46 @@ postfix() {
|
|
|
724
1324
|
const startLine = t.line;
|
|
725
1325
|
const startCol = t.column;
|
|
726
1326
|
this.eat('LPAREN');
|
|
1327
|
+
|
|
727
1328
|
const args = [];
|
|
728
|
-
|
|
1329
|
+
|
|
1330
|
+
while (this.current.type !== 'RPAREN') {
|
|
1331
|
+
if (this.current.type === 'EOF') {
|
|
1332
|
+
throw new ParseError(
|
|
1333
|
+
"Unterminated function call",
|
|
1334
|
+
this.current,
|
|
1335
|
+
this.source,
|
|
1336
|
+
"Did you forget to close ')'?"
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
729
1340
|
args.push(this.expression());
|
|
730
|
-
|
|
1341
|
+
|
|
1342
|
+
if (this.current.type === 'COMMA') {
|
|
1343
|
+
this.eat('COMMA');
|
|
1344
|
+
} else {
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
731
1347
|
}
|
|
732
|
-
|
|
733
|
-
|
|
1348
|
+
|
|
1349
|
+
if (this.current.type !== 'RPAREN') {
|
|
1350
|
+
throw new ParseError(
|
|
1351
|
+
"Expected ')' after function arguments",
|
|
1352
|
+
this.current,
|
|
1353
|
+
this.source,
|
|
1354
|
+
"Function calls must be closed with ')'"
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
this.eat('RPAREN');
|
|
1359
|
+
|
|
1360
|
+
node = {
|
|
1361
|
+
type: 'CallExpression',
|
|
1362
|
+
callee: node,
|
|
1363
|
+
arguments: args,
|
|
1364
|
+
line: startLine,
|
|
1365
|
+
column: startCol
|
|
1366
|
+
};
|
|
734
1367
|
continue;
|
|
735
1368
|
}
|
|
736
1369
|
|
|
@@ -738,21 +1371,46 @@ postfix() {
|
|
|
738
1371
|
const startLine = t.line;
|
|
739
1372
|
const startCol = t.column;
|
|
740
1373
|
this.eat('DOT');
|
|
741
|
-
|
|
742
|
-
if (this.current.type
|
|
743
|
-
|
|
1374
|
+
|
|
1375
|
+
if (this.current.type !== 'IDENTIFIER') {
|
|
1376
|
+
throw new ParseError(
|
|
1377
|
+
"Expected property name after '.'",
|
|
1378
|
+
this.current,
|
|
1379
|
+
this.source,
|
|
1380
|
+
"Member access requires a property name, e.g. obj.value"
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const property = this.current.value;
|
|
1385
|
+
this.eat('IDENTIFIER');
|
|
1386
|
+
|
|
1387
|
+
node = {
|
|
1388
|
+
type: 'MemberExpression',
|
|
1389
|
+
object: node,
|
|
1390
|
+
property,
|
|
1391
|
+
line: startLine,
|
|
1392
|
+
column: startCol
|
|
1393
|
+
};
|
|
744
1394
|
continue;
|
|
745
1395
|
}
|
|
746
1396
|
|
|
747
1397
|
if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
|
|
748
1398
|
this.eat(t.type);
|
|
749
|
-
|
|
1399
|
+
|
|
1400
|
+
node = {
|
|
1401
|
+
type: 'UpdateExpression',
|
|
1402
|
+
operator: t.type,
|
|
1403
|
+
argument: node,
|
|
1404
|
+
prefix: false,
|
|
1405
|
+
line: t.line,
|
|
1406
|
+
column: t.column
|
|
1407
|
+
};
|
|
750
1408
|
continue;
|
|
751
1409
|
}
|
|
752
1410
|
|
|
753
|
-
|
|
754
1411
|
break;
|
|
755
1412
|
}
|
|
1413
|
+
|
|
756
1414
|
return node;
|
|
757
1415
|
}
|
|
758
1416
|
|
|
@@ -764,10 +1422,19 @@ arrowFunction(params) {
|
|
|
764
1422
|
let isBlock = false;
|
|
765
1423
|
|
|
766
1424
|
if (this.current.type === 'LBRACE') {
|
|
767
|
-
body = this.block();
|
|
1425
|
+
body = this.block();
|
|
768
1426
|
isBlock = true;
|
|
769
|
-
}
|
|
770
|
-
|
|
1427
|
+
}
|
|
1428
|
+
else if (this.current.type === 'EOF') {
|
|
1429
|
+
throw new ParseError(
|
|
1430
|
+
"Missing arrow function body",
|
|
1431
|
+
this.current,
|
|
1432
|
+
this.source,
|
|
1433
|
+
"Arrow functions require a body, e.g. x => x * 2"
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
body = this.expression();
|
|
771
1438
|
}
|
|
772
1439
|
|
|
773
1440
|
const startLine = params.length > 0 ? params[0].line : t.line;
|
|
@@ -783,75 +1450,116 @@ arrowFunction(params) {
|
|
|
783
1450
|
};
|
|
784
1451
|
}
|
|
785
1452
|
|
|
786
|
-
|
|
787
|
-
primary() {
|
|
1453
|
+
primary() {
|
|
788
1454
|
const t = this.current;
|
|
789
1455
|
|
|
790
|
-
if (t.type === 'NUMBER') {
|
|
791
|
-
this.eat('NUMBER');
|
|
792
|
-
return { type: 'Literal', value: t.value, line: t.line, column: t.column };
|
|
1456
|
+
if (t.type === 'NUMBER') {
|
|
1457
|
+
this.eat('NUMBER');
|
|
1458
|
+
return { type: 'Literal', value: t.value, line: t.line, column: t.column };
|
|
793
1459
|
}
|
|
794
1460
|
|
|
795
|
-
if (t.type === 'STRING') {
|
|
796
|
-
this.eat('STRING');
|
|
797
|
-
return { type: 'Literal', value: t.value, line: t.line, column: t.column };
|
|
1461
|
+
if (t.type === 'STRING') {
|
|
1462
|
+
this.eat('STRING');
|
|
1463
|
+
return { type: 'Literal', value: t.value, line: t.line, column: t.column };
|
|
798
1464
|
}
|
|
799
1465
|
|
|
800
|
-
if (t.type === 'TRUE') {
|
|
801
|
-
this.eat('TRUE');
|
|
802
|
-
return { type: 'Literal', value: true, line: t.line, column: t.column };
|
|
1466
|
+
if (t.type === 'TRUE') {
|
|
1467
|
+
this.eat('TRUE');
|
|
1468
|
+
return { type: 'Literal', value: true, line: t.line, column: t.column };
|
|
803
1469
|
}
|
|
804
1470
|
|
|
805
|
-
if (t.type === 'FALSE') {
|
|
806
|
-
this.eat('FALSE');
|
|
807
|
-
return { type: 'Literal', value: false, line: t.line, column: t.column };
|
|
1471
|
+
if (t.type === 'FALSE') {
|
|
1472
|
+
this.eat('FALSE');
|
|
1473
|
+
return { type: 'Literal', value: false, line: t.line, column: t.column };
|
|
808
1474
|
}
|
|
809
1475
|
|
|
810
|
-
if (t.type === 'NULL') {
|
|
811
|
-
this.eat('NULL');
|
|
812
|
-
return { type: 'Literal', value: null, line: t.line, column: t.column };
|
|
1476
|
+
if (t.type === 'NULL') {
|
|
1477
|
+
this.eat('NULL');
|
|
1478
|
+
return { type: 'Literal', value: null, line: t.line, column: t.column };
|
|
813
1479
|
}
|
|
814
1480
|
|
|
815
1481
|
if (t.type === 'AWAIT') {
|
|
816
1482
|
this.eat('AWAIT');
|
|
817
|
-
const argument = this.expression();
|
|
1483
|
+
const argument = this.expression();
|
|
818
1484
|
return { type: 'AwaitExpression', argument, line: t.line, column: t.column };
|
|
819
1485
|
}
|
|
820
1486
|
|
|
821
1487
|
if (t.type === 'NEW') {
|
|
822
1488
|
this.eat('NEW');
|
|
823
1489
|
const callee = this.primary();
|
|
1490
|
+
|
|
1491
|
+
if (this.current.type !== 'LPAREN') {
|
|
1492
|
+
throw new ParseError(
|
|
1493
|
+
"Expected '(' after 'new'",
|
|
1494
|
+
this.current,
|
|
1495
|
+
this.source,
|
|
1496
|
+
"Use 'new ClassName(...)'"
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
824
1500
|
this.eat('LPAREN');
|
|
825
1501
|
const args = [];
|
|
826
1502
|
if (this.current.type !== 'RPAREN') {
|
|
827
1503
|
args.push(this.expression());
|
|
828
1504
|
while (this.current.type === 'COMMA') {
|
|
829
1505
|
this.eat('COMMA');
|
|
830
|
-
if (this.current.type
|
|
1506
|
+
if (this.current.type === 'RPAREN') break;
|
|
1507
|
+
args.push(this.expression());
|
|
831
1508
|
}
|
|
832
1509
|
}
|
|
1510
|
+
|
|
1511
|
+
if (this.current.type !== 'RPAREN') {
|
|
1512
|
+
throw new ParseError(
|
|
1513
|
+
"Expected ')' to close arguments for 'new'",
|
|
1514
|
+
this.current,
|
|
1515
|
+
this.source
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
833
1519
|
this.eat('RPAREN');
|
|
1520
|
+
|
|
834
1521
|
return { type: 'NewExpression', callee, arguments: args, line: t.line, column: t.column };
|
|
835
1522
|
}
|
|
836
1523
|
|
|
1524
|
+
// ---- ask(...) function call ----
|
|
837
1525
|
if (t.type === 'ASK') {
|
|
838
1526
|
this.eat('ASK');
|
|
1527
|
+
|
|
1528
|
+
if (this.current.type !== 'LPAREN') {
|
|
1529
|
+
throw new ParseError(
|
|
1530
|
+
"Expected '(' after 'ask'",
|
|
1531
|
+
this.current,
|
|
1532
|
+
this.source
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
839
1536
|
this.eat('LPAREN');
|
|
840
1537
|
const args = [];
|
|
841
1538
|
if (this.current.type !== 'RPAREN') {
|
|
842
1539
|
args.push(this.expression());
|
|
843
1540
|
while (this.current.type === 'COMMA') {
|
|
844
1541
|
this.eat('COMMA');
|
|
845
|
-
if (this.current.type
|
|
1542
|
+
if (this.current.type === 'RPAREN') break;
|
|
1543
|
+
args.push(this.expression());
|
|
846
1544
|
}
|
|
847
1545
|
}
|
|
1546
|
+
|
|
1547
|
+
if (this.current.type !== 'RPAREN') {
|
|
1548
|
+
throw new ParseError(
|
|
1549
|
+
"Expected ')' after arguments to 'ask'",
|
|
1550
|
+
this.current,
|
|
1551
|
+
this.source
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
848
1555
|
this.eat('RPAREN');
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1556
|
+
|
|
1557
|
+
return {
|
|
1558
|
+
type: 'CallExpression',
|
|
1559
|
+
callee: { type: 'Identifier', name: 'ask', line: t.line, column: t.column },
|
|
1560
|
+
arguments: args,
|
|
1561
|
+
line: t.line,
|
|
1562
|
+
column: t.column
|
|
855
1563
|
};
|
|
856
1564
|
}
|
|
857
1565
|
|
|
@@ -870,89 +1578,120 @@ arrowFunction(params) {
|
|
|
870
1578
|
const startLine = t.line;
|
|
871
1579
|
const startCol = t.column;
|
|
872
1580
|
this.eat('LPAREN');
|
|
873
|
-
const elements = [];
|
|
874
1581
|
|
|
1582
|
+
const elements = [];
|
|
875
1583
|
if (this.current.type !== 'RPAREN') {
|
|
876
1584
|
elements.push(this.expression());
|
|
877
1585
|
while (this.current.type === 'COMMA') {
|
|
878
1586
|
this.eat('COMMA');
|
|
879
|
-
if (this.current.type
|
|
1587
|
+
if (this.current.type === 'RPAREN') break;
|
|
1588
|
+
elements.push(this.expression());
|
|
880
1589
|
}
|
|
881
1590
|
}
|
|
882
1591
|
|
|
1592
|
+
if (this.current.type !== 'RPAREN') {
|
|
1593
|
+
throw new ParseError(
|
|
1594
|
+
"Expected ')' after expression",
|
|
1595
|
+
this.current,
|
|
1596
|
+
this.source
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
883
1600
|
this.eat('RPAREN');
|
|
884
1601
|
|
|
885
|
-
if (this.current.type === 'ARROW')
|
|
1602
|
+
if (this.current.type === 'ARROW') {
|
|
1603
|
+
return this.arrowFunction(elements);
|
|
1604
|
+
}
|
|
886
1605
|
|
|
887
|
-
return elements.length === 1
|
|
1606
|
+
return elements.length === 1
|
|
1607
|
+
? elements[0]
|
|
1608
|
+
: { type: 'ArrayExpression', elements, line: startLine, column: startCol };
|
|
888
1609
|
}
|
|
889
1610
|
|
|
890
1611
|
if (t.type === 'LBRACKET') {
|
|
891
1612
|
const startLine = t.line;
|
|
892
1613
|
const startCol = t.column;
|
|
893
1614
|
this.eat('LBRACKET');
|
|
1615
|
+
|
|
894
1616
|
const elements = [];
|
|
895
1617
|
if (this.current.type !== 'RBRACKET') {
|
|
896
1618
|
elements.push(this.expression());
|
|
897
1619
|
while (this.current.type === 'COMMA') {
|
|
898
1620
|
this.eat('COMMA');
|
|
899
|
-
if (this.current.type
|
|
1621
|
+
if (this.current.type === 'RBRACKET') break;
|
|
1622
|
+
elements.push(this.expression());
|
|
900
1623
|
}
|
|
901
1624
|
}
|
|
1625
|
+
|
|
1626
|
+
if (this.current.type !== 'RBRACKET') {
|
|
1627
|
+
throw new ParseError(
|
|
1628
|
+
"Expected ']' after array elements",
|
|
1629
|
+
this.current,
|
|
1630
|
+
this.source
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
902
1634
|
this.eat('RBRACKET');
|
|
1635
|
+
|
|
903
1636
|
return { type: 'ArrayExpression', elements, line: startLine, column: startCol };
|
|
904
1637
|
}
|
|
905
1638
|
|
|
906
1639
|
if (t.type === 'LBRACE') {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1640
|
+
const startLine = t.line;
|
|
1641
|
+
const startCol = t.column;
|
|
1642
|
+
this.eat('LBRACE');
|
|
910
1643
|
|
|
911
|
-
|
|
1644
|
+
const props = [];
|
|
912
1645
|
|
|
913
|
-
|
|
914
|
-
|
|
1646
|
+
while (this.current.type !== 'RBRACE') {
|
|
1647
|
+
if (this.current.type === 'EOF') {
|
|
1648
|
+
throw new ParseError(
|
|
1649
|
+
"Unterminated object literal",
|
|
1650
|
+
this.current,
|
|
1651
|
+
this.source,
|
|
1652
|
+
"Did you forget to close '}'?"
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
915
1655
|
|
|
916
|
-
|
|
917
|
-
const k = this.current;
|
|
918
|
-
this.eat('IDENTIFIER');
|
|
919
|
-
key = {
|
|
920
|
-
type: 'Literal',
|
|
921
|
-
value: k.value,
|
|
922
|
-
line: k.line,
|
|
923
|
-
column: k.column
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
else if (this.current.type === 'STRING') {
|
|
927
|
-
const k = this.current;
|
|
928
|
-
this.eat('STRING');
|
|
929
|
-
key = {
|
|
930
|
-
type: 'Literal',
|
|
931
|
-
value: k.value,
|
|
932
|
-
line: k.line,
|
|
933
|
-
column: k.column
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
else {
|
|
937
|
-
throw new ParseError(
|
|
938
|
-
'Invalid object key',
|
|
939
|
-
this.current,
|
|
940
|
-
this.source
|
|
941
|
-
);
|
|
942
|
-
}
|
|
1656
|
+
let key;
|
|
943
1657
|
|
|
944
|
-
|
|
945
|
-
|
|
1658
|
+
if (this.current.type === 'IDENTIFIER') {
|
|
1659
|
+
const k = this.current;
|
|
1660
|
+
this.eat('IDENTIFIER');
|
|
1661
|
+
key = { type: 'Literal', value: k.value, line: k.line, column: k.column };
|
|
1662
|
+
}
|
|
1663
|
+
else if (this.current.type === 'STRING') {
|
|
1664
|
+
const k = this.current;
|
|
1665
|
+
this.eat('STRING');
|
|
1666
|
+
key = { type: 'Literal', value: k.value, line: k.line, column: k.column };
|
|
1667
|
+
}
|
|
1668
|
+
else {
|
|
1669
|
+
throw new ParseError(
|
|
1670
|
+
'Invalid object key',
|
|
1671
|
+
this.current,
|
|
1672
|
+
this.source,
|
|
1673
|
+
"Object keys must be identifiers or strings"
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
946
1676
|
|
|
947
|
-
|
|
1677
|
+
if (this.current.type !== 'COLON') {
|
|
1678
|
+
throw new ParseError(
|
|
1679
|
+
"Expected ':' after object key",
|
|
1680
|
+
this.current,
|
|
1681
|
+
this.source
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
948
1684
|
|
|
949
|
-
|
|
950
|
-
|
|
1685
|
+
this.eat('COLON');
|
|
1686
|
+
const value = this.expression();
|
|
1687
|
+
props.push({ key, value });
|
|
951
1688
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
1689
|
+
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
1690
|
+
}
|
|
955
1691
|
|
|
1692
|
+
this.eat('RBRACE');
|
|
1693
|
+
return { type: 'ObjectExpression', props, line: startLine, column: startCol };
|
|
1694
|
+
}
|
|
956
1695
|
|
|
957
1696
|
throw new ParseError(
|
|
958
1697
|
`Unexpected token '${t.type}'`,
|
|
@@ -961,7 +1700,6 @@ arrowFunction(params) {
|
|
|
961
1700
|
);
|
|
962
1701
|
}
|
|
963
1702
|
|
|
964
|
-
|
|
965
1703
|
}
|
|
966
1704
|
|
|
967
1705
|
module.exports = Parser;
|