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