starlight-cli 1.1.10 → 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 +1574 -520
- package/package.json +1 -1
- package/src/evaluator.js +648 -355
- package/src/lexer.js +3 -1
- package/src/parser.js +917 -158
- 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',
|
|
@@ -279,20 +518,82 @@ ifStatement() {
|
|
|
279
518
|
};
|
|
280
519
|
}
|
|
281
520
|
|
|
521
|
+
parseExpressionOnly() {
|
|
522
|
+
return this.expression();
|
|
523
|
+
}
|
|
524
|
+
|
|
282
525
|
whileStatement() {
|
|
283
526
|
const t = this.current;
|
|
284
527
|
this.eat('WHILE');
|
|
285
528
|
|
|
286
529
|
let test;
|
|
530
|
+
|
|
287
531
|
if (this.current.type === 'LPAREN') {
|
|
288
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
|
+
|
|
289
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
|
+
|
|
290
554
|
this.eat('RPAREN');
|
|
291
|
-
}
|
|
292
|
-
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
const exprTokens = [];
|
|
558
|
+
let braceFound = false;
|
|
559
|
+
let depth = 0;
|
|
560
|
+
|
|
561
|
+
while (this.current.type !== 'EOF') {
|
|
562
|
+
if (this.current.type === 'LBRACE' && depth === 0) {
|
|
563
|
+
braceFound = true;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
if (this.current.type === 'LPAREN') depth++;
|
|
567
|
+
if (this.current.type === 'RPAREN') depth--;
|
|
568
|
+
exprTokens.push(this.current);
|
|
569
|
+
this.advance();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!braceFound) {
|
|
573
|
+
throw new ParseError(
|
|
574
|
+
"Expected '{' after while condition",
|
|
575
|
+
this.current,
|
|
576
|
+
this.source,
|
|
577
|
+
"While loops must be followed by a block"
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const Parser = require('./parser');
|
|
582
|
+
const exprParser = new Parser(exprTokens, this.source);
|
|
583
|
+
test = exprParser.parseExpressionOnly();
|
|
584
|
+
}
|
|
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
|
+
);
|
|
293
593
|
}
|
|
294
594
|
|
|
295
595
|
const body = this.block();
|
|
596
|
+
|
|
296
597
|
return {
|
|
297
598
|
type: 'WhileStatement',
|
|
298
599
|
test,
|
|
@@ -307,15 +608,62 @@ importStatement() {
|
|
|
307
608
|
this.eat('IMPORT');
|
|
308
609
|
|
|
309
610
|
let specifiers = [];
|
|
611
|
+
|
|
310
612
|
if (this.current.type === 'STAR') {
|
|
311
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
|
+
|
|
312
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
|
+
|
|
313
635
|
const name = this.current.value;
|
|
314
636
|
this.eat('IDENTIFIER');
|
|
315
|
-
|
|
637
|
+
|
|
638
|
+
specifiers.push({
|
|
639
|
+
type: 'NamespaceImport',
|
|
640
|
+
local: name,
|
|
641
|
+
line: t.line,
|
|
642
|
+
column: t.column
|
|
643
|
+
});
|
|
644
|
+
|
|
316
645
|
} else if (this.current.type === 'LBRACE') {
|
|
317
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
|
+
|
|
318
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
|
+
|
|
319
667
|
const importedName = this.current.value;
|
|
320
668
|
const importedLine = this.current.line;
|
|
321
669
|
const importedColumn = this.current.column;
|
|
@@ -324,27 +672,77 @@ importStatement() {
|
|
|
324
672
|
let localName = importedName;
|
|
325
673
|
if (this.current.type === 'AS') {
|
|
326
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
|
+
|
|
327
685
|
localName = this.current.value;
|
|
328
686
|
this.eat('IDENTIFIER');
|
|
329
687
|
}
|
|
330
688
|
|
|
331
|
-
specifiers.push({
|
|
689
|
+
specifiers.push({
|
|
690
|
+
type: 'NamedImport',
|
|
691
|
+
imported: importedName,
|
|
692
|
+
local: localName,
|
|
693
|
+
line: importedLine,
|
|
694
|
+
column: importedColumn
|
|
695
|
+
});
|
|
696
|
+
|
|
332
697
|
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
333
698
|
}
|
|
699
|
+
|
|
334
700
|
this.eat('RBRACE');
|
|
701
|
+
|
|
335
702
|
} else if (this.current.type === 'IDENTIFIER') {
|
|
336
703
|
const localName = this.current.value;
|
|
337
704
|
const localLine = this.current.line;
|
|
338
705
|
const localColumn = this.current.column;
|
|
339
706
|
this.eat('IDENTIFIER');
|
|
340
|
-
|
|
707
|
+
|
|
708
|
+
specifiers.push({
|
|
709
|
+
type: 'DefaultImport',
|
|
710
|
+
local: localName,
|
|
711
|
+
line: localLine,
|
|
712
|
+
column: localColumn
|
|
713
|
+
});
|
|
714
|
+
|
|
341
715
|
} else {
|
|
342
|
-
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
|
+
);
|
|
343
731
|
}
|
|
344
732
|
|
|
345
733
|
this.eat('FROM');
|
|
734
|
+
|
|
346
735
|
const pathToken = this.current;
|
|
347
|
-
|
|
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
|
+
|
|
348
746
|
this.eat('STRING');
|
|
349
747
|
|
|
350
748
|
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
@@ -357,14 +755,14 @@ importStatement() {
|
|
|
357
755
|
column: t.column
|
|
358
756
|
};
|
|
359
757
|
}
|
|
360
|
-
|
|
361
758
|
forStatement() {
|
|
362
|
-
const t = this.current;
|
|
759
|
+
const t = this.current;
|
|
363
760
|
this.eat('FOR');
|
|
364
761
|
|
|
365
|
-
if (
|
|
366
|
-
|
|
367
|
-
|
|
762
|
+
if (
|
|
763
|
+
(this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
|
|
764
|
+
(this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')
|
|
765
|
+
) {
|
|
368
766
|
let variable;
|
|
369
767
|
let variableLine, variableColumn;
|
|
370
768
|
let iterable;
|
|
@@ -373,22 +771,45 @@ forStatement() {
|
|
|
373
771
|
if (this.current.type === 'LET') {
|
|
374
772
|
letKeyword = true;
|
|
375
773
|
this.eat('LET');
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
);
|
|
389
809
|
}
|
|
390
810
|
|
|
391
811
|
const body = this.block();
|
|
812
|
+
|
|
392
813
|
return {
|
|
393
814
|
type: 'ForInStatement',
|
|
394
815
|
variable,
|
|
@@ -410,25 +831,53 @@ forStatement() {
|
|
|
410
831
|
this.eat('LPAREN');
|
|
411
832
|
|
|
412
833
|
if (this.current.type !== 'SEMICOLON') {
|
|
413
|
-
init = this.current.type === 'LET'
|
|
834
|
+
init = this.current.type === 'LET'
|
|
835
|
+
? this.varDeclaration()
|
|
836
|
+
: this.expressionStatement();
|
|
414
837
|
} else {
|
|
415
838
|
this.eat('SEMICOLON');
|
|
416
839
|
}
|
|
417
840
|
|
|
418
|
-
if (this.current.type !== 'SEMICOLON')
|
|
841
|
+
if (this.current.type !== 'SEMICOLON') {
|
|
842
|
+
test = this.expression();
|
|
843
|
+
}
|
|
844
|
+
|
|
419
845
|
this.eat('SEMICOLON');
|
|
420
846
|
|
|
421
|
-
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
|
+
|
|
422
860
|
this.eat('RPAREN');
|
|
423
861
|
} else {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
this.
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
);
|
|
429
877
|
}
|
|
430
878
|
|
|
431
879
|
const body = this.block();
|
|
880
|
+
|
|
432
881
|
return {
|
|
433
882
|
type: 'ForStatement',
|
|
434
883
|
init,
|
|
@@ -453,41 +902,111 @@ continueStatement() {
|
|
|
453
902
|
if (this.current.type === 'SEMICOLON') this.advance();
|
|
454
903
|
return { type: 'ContinueStatement', line: t.line, column: t.column };
|
|
455
904
|
}
|
|
456
|
-
|
|
457
905
|
funcDeclaration() {
|
|
458
906
|
const t = this.current;
|
|
459
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
|
+
|
|
460
918
|
const nameToken = this.current;
|
|
461
919
|
const name = nameToken.value;
|
|
462
920
|
this.eat('IDENTIFIER');
|
|
463
921
|
|
|
464
922
|
let params = [];
|
|
923
|
+
|
|
465
924
|
if (this.current.type === 'LPAREN') {
|
|
466
925
|
this.eat('LPAREN');
|
|
926
|
+
|
|
467
927
|
if (this.current.type !== 'RPAREN') {
|
|
468
|
-
|
|
469
|
-
|
|
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
|
+
});
|
|
470
943
|
this.eat('IDENTIFIER');
|
|
944
|
+
|
|
471
945
|
while (this.current.type === 'COMMA') {
|
|
472
946
|
this.eat('COMMA');
|
|
473
|
-
|
|
474
|
-
|
|
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
|
+
});
|
|
475
963
|
this.eat('IDENTIFIER');
|
|
476
964
|
}
|
|
477
965
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
);
|
|
484
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
|
+
);
|
|
485
995
|
}
|
|
486
996
|
|
|
487
997
|
const body = this.block();
|
|
488
|
-
|
|
998
|
+
|
|
999
|
+
return {
|
|
1000
|
+
type: 'FunctionDeclaration',
|
|
1001
|
+
name,
|
|
1002
|
+
params,
|
|
1003
|
+
body,
|
|
1004
|
+
line: t.line,
|
|
1005
|
+
column: t.column
|
|
1006
|
+
};
|
|
489
1007
|
}
|
|
490
1008
|
|
|
1009
|
+
|
|
491
1010
|
returnStatement() {
|
|
492
1011
|
const t = this.current; // RETURN token
|
|
493
1012
|
this.eat('RETURN');
|
|
@@ -507,18 +1026,35 @@ statementOrBlock() {
|
|
|
507
1026
|
}
|
|
508
1027
|
return this.statement();
|
|
509
1028
|
}
|
|
510
|
-
|
|
511
1029
|
block() {
|
|
512
1030
|
const t = this.current; // LBRACE token
|
|
513
1031
|
this.eat('LBRACE');
|
|
1032
|
+
|
|
514
1033
|
const body = [];
|
|
1034
|
+
|
|
515
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
|
+
}
|
|
516
1044
|
body.push(this.statement());
|
|
517
1045
|
}
|
|
1046
|
+
|
|
518
1047
|
this.eat('RBRACE');
|
|
519
|
-
|
|
1048
|
+
|
|
1049
|
+
return {
|
|
1050
|
+
type: 'BlockStatement',
|
|
1051
|
+
body,
|
|
1052
|
+
line: t.line,
|
|
1053
|
+
column: t.column
|
|
1054
|
+
};
|
|
520
1055
|
}
|
|
521
1056
|
|
|
1057
|
+
|
|
522
1058
|
expressionStatement() {
|
|
523
1059
|
const exprToken = this.current;
|
|
524
1060
|
const expr = this.expression();
|
|
@@ -529,7 +1065,6 @@ expressionStatement() {
|
|
|
529
1065
|
expression() {
|
|
530
1066
|
return this.assignment();
|
|
531
1067
|
}
|
|
532
|
-
|
|
533
1068
|
assignment() {
|
|
534
1069
|
const node = this.ternary();
|
|
535
1070
|
const compoundOps = ['PLUSEQ', 'MINUSEQ', 'STAREQ', 'SLASHEQ', 'MODEQ'];
|
|
@@ -540,29 +1075,76 @@ assignment() {
|
|
|
540
1075
|
const op = t.type;
|
|
541
1076
|
this.eat(op);
|
|
542
1077
|
const right = this.assignment();
|
|
543
|
-
|
|
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
|
+
);
|
|
544
1096
|
}
|
|
545
1097
|
|
|
546
1098
|
if (t.type === 'EQUAL') {
|
|
547
1099
|
this.eat('EQUAL');
|
|
548
1100
|
const right = this.assignment();
|
|
549
|
-
|
|
1101
|
+
|
|
1102
|
+
return {
|
|
1103
|
+
type: 'AssignmentExpression',
|
|
1104
|
+
left: node,
|
|
1105
|
+
right,
|
|
1106
|
+
line: t.line,
|
|
1107
|
+
column: t.column
|
|
1108
|
+
};
|
|
550
1109
|
}
|
|
551
1110
|
|
|
552
1111
|
return node;
|
|
553
1112
|
}
|
|
1113
|
+
|
|
554
1114
|
ternary() {
|
|
555
1115
|
let node = this.nullishCoalescing();
|
|
1116
|
+
|
|
556
1117
|
while (this.current.type === 'QUESTION') {
|
|
557
1118
|
const t = this.current;
|
|
558
1119
|
this.eat('QUESTION');
|
|
559
|
-
|
|
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
|
+
|
|
560
1132
|
this.eat('COLON');
|
|
561
|
-
const alternate = this.expression();
|
|
562
|
-
|
|
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
|
+
};
|
|
563
1143
|
}
|
|
1144
|
+
|
|
564
1145
|
return node;
|
|
565
1146
|
}
|
|
1147
|
+
|
|
566
1148
|
nullishCoalescing() {
|
|
567
1149
|
let node = this.logicalOr();
|
|
568
1150
|
while (this.current.type === 'NULLISH_COALESCING') {
|
|
@@ -645,6 +1227,16 @@ unary() {
|
|
|
645
1227
|
if (['NOT', 'MINUS', 'PLUS'].includes(t.type)) {
|
|
646
1228
|
const op = t.type;
|
|
647
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
|
+
|
|
648
1240
|
return {
|
|
649
1241
|
type: 'UnaryExpression',
|
|
650
1242
|
operator: op,
|
|
@@ -657,7 +1249,18 @@ unary() {
|
|
|
657
1249
|
if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
|
|
658
1250
|
const op = t.type;
|
|
659
1251
|
this.eat(op);
|
|
1252
|
+
|
|
660
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
|
+
|
|
661
1264
|
return {
|
|
662
1265
|
type: 'UpdateExpression',
|
|
663
1266
|
operator: op,
|
|
@@ -670,9 +1273,9 @@ unary() {
|
|
|
670
1273
|
|
|
671
1274
|
return this.postfix();
|
|
672
1275
|
}
|
|
673
|
-
|
|
674
1276
|
postfix() {
|
|
675
1277
|
let node = this.primary();
|
|
1278
|
+
|
|
676
1279
|
while (true) {
|
|
677
1280
|
const t = this.current;
|
|
678
1281
|
|
|
@@ -680,9 +1283,27 @@ postfix() {
|
|
|
680
1283
|
const startLine = t.line;
|
|
681
1284
|
const startCol = t.column;
|
|
682
1285
|
this.eat('LBRACKET');
|
|
1286
|
+
|
|
683
1287
|
const index = this.expression();
|
|
684
|
-
|
|
685
|
-
|
|
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
|
+
};
|
|
686
1307
|
continue;
|
|
687
1308
|
}
|
|
688
1309
|
|
|
@@ -690,13 +1311,46 @@ postfix() {
|
|
|
690
1311
|
const startLine = t.line;
|
|
691
1312
|
const startCol = t.column;
|
|
692
1313
|
this.eat('LPAREN');
|
|
1314
|
+
|
|
693
1315
|
const args = [];
|
|
694
|
-
|
|
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
|
+
|
|
695
1327
|
args.push(this.expression());
|
|
696
|
-
|
|
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
|
+
);
|
|
697
1343
|
}
|
|
698
|
-
|
|
699
|
-
|
|
1344
|
+
|
|
1345
|
+
this.eat('RPAREN');
|
|
1346
|
+
|
|
1347
|
+
node = {
|
|
1348
|
+
type: 'CallExpression',
|
|
1349
|
+
callee: node,
|
|
1350
|
+
arguments: args,
|
|
1351
|
+
line: startLine,
|
|
1352
|
+
column: startCol
|
|
1353
|
+
};
|
|
700
1354
|
continue;
|
|
701
1355
|
}
|
|
702
1356
|
|
|
@@ -704,21 +1358,46 @@ postfix() {
|
|
|
704
1358
|
const startLine = t.line;
|
|
705
1359
|
const startCol = t.column;
|
|
706
1360
|
this.eat('DOT');
|
|
707
|
-
|
|
708
|
-
if (this.current.type
|
|
709
|
-
|
|
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
|
+
};
|
|
710
1381
|
continue;
|
|
711
1382
|
}
|
|
712
1383
|
|
|
713
1384
|
if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
|
|
714
1385
|
this.eat(t.type);
|
|
715
|
-
|
|
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
|
+
};
|
|
716
1395
|
continue;
|
|
717
1396
|
}
|
|
718
1397
|
|
|
719
|
-
|
|
720
1398
|
break;
|
|
721
1399
|
}
|
|
1400
|
+
|
|
722
1401
|
return node;
|
|
723
1402
|
}
|
|
724
1403
|
|
|
@@ -730,10 +1409,19 @@ arrowFunction(params) {
|
|
|
730
1409
|
let isBlock = false;
|
|
731
1410
|
|
|
732
1411
|
if (this.current.type === 'LBRACE') {
|
|
733
|
-
body = this.block();
|
|
1412
|
+
body = this.block();
|
|
734
1413
|
isBlock = true;
|
|
735
|
-
}
|
|
736
|
-
|
|
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();
|
|
737
1425
|
}
|
|
738
1426
|
|
|
739
1427
|
const startLine = params.length > 0 ? params[0].line : t.line;
|
|
@@ -749,75 +1437,116 @@ arrowFunction(params) {
|
|
|
749
1437
|
};
|
|
750
1438
|
}
|
|
751
1439
|
|
|
752
|
-
|
|
753
|
-
primary() {
|
|
1440
|
+
primary() {
|
|
754
1441
|
const t = this.current;
|
|
755
1442
|
|
|
756
|
-
if (t.type === 'NUMBER') {
|
|
757
|
-
this.eat('NUMBER');
|
|
758
|
-
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 };
|
|
759
1446
|
}
|
|
760
1447
|
|
|
761
|
-
if (t.type === 'STRING') {
|
|
762
|
-
this.eat('STRING');
|
|
763
|
-
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 };
|
|
764
1451
|
}
|
|
765
1452
|
|
|
766
|
-
if (t.type === 'TRUE') {
|
|
767
|
-
this.eat('TRUE');
|
|
768
|
-
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 };
|
|
769
1456
|
}
|
|
770
1457
|
|
|
771
|
-
if (t.type === 'FALSE') {
|
|
772
|
-
this.eat('FALSE');
|
|
773
|
-
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 };
|
|
774
1461
|
}
|
|
775
1462
|
|
|
776
|
-
if (t.type === 'NULL') {
|
|
777
|
-
this.eat('NULL');
|
|
778
|
-
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 };
|
|
779
1466
|
}
|
|
780
1467
|
|
|
781
1468
|
if (t.type === 'AWAIT') {
|
|
782
1469
|
this.eat('AWAIT');
|
|
783
|
-
const argument = this.expression();
|
|
1470
|
+
const argument = this.expression();
|
|
784
1471
|
return { type: 'AwaitExpression', argument, line: t.line, column: t.column };
|
|
785
1472
|
}
|
|
786
1473
|
|
|
787
1474
|
if (t.type === 'NEW') {
|
|
788
1475
|
this.eat('NEW');
|
|
789
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
|
+
|
|
790
1487
|
this.eat('LPAREN');
|
|
791
1488
|
const args = [];
|
|
792
1489
|
if (this.current.type !== 'RPAREN') {
|
|
793
1490
|
args.push(this.expression());
|
|
794
1491
|
while (this.current.type === 'COMMA') {
|
|
795
1492
|
this.eat('COMMA');
|
|
796
|
-
if (this.current.type
|
|
1493
|
+
if (this.current.type === 'RPAREN') break;
|
|
1494
|
+
args.push(this.expression());
|
|
797
1495
|
}
|
|
798
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
|
+
|
|
799
1506
|
this.eat('RPAREN');
|
|
1507
|
+
|
|
800
1508
|
return { type: 'NewExpression', callee, arguments: args, line: t.line, column: t.column };
|
|
801
1509
|
}
|
|
802
1510
|
|
|
1511
|
+
// ---- ask(...) function call ----
|
|
803
1512
|
if (t.type === 'ASK') {
|
|
804
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
|
+
|
|
805
1523
|
this.eat('LPAREN');
|
|
806
1524
|
const args = [];
|
|
807
1525
|
if (this.current.type !== 'RPAREN') {
|
|
808
1526
|
args.push(this.expression());
|
|
809
1527
|
while (this.current.type === 'COMMA') {
|
|
810
1528
|
this.eat('COMMA');
|
|
811
|
-
if (this.current.type
|
|
1529
|
+
if (this.current.type === 'RPAREN') break;
|
|
1530
|
+
args.push(this.expression());
|
|
812
1531
|
}
|
|
813
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
|
+
|
|
814
1542
|
this.eat('RPAREN');
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
|
821
1550
|
};
|
|
822
1551
|
}
|
|
823
1552
|
|
|
@@ -836,89 +1565,120 @@ arrowFunction(params) {
|
|
|
836
1565
|
const startLine = t.line;
|
|
837
1566
|
const startCol = t.column;
|
|
838
1567
|
this.eat('LPAREN');
|
|
839
|
-
const elements = [];
|
|
840
1568
|
|
|
1569
|
+
const elements = [];
|
|
841
1570
|
if (this.current.type !== 'RPAREN') {
|
|
842
1571
|
elements.push(this.expression());
|
|
843
1572
|
while (this.current.type === 'COMMA') {
|
|
844
1573
|
this.eat('COMMA');
|
|
845
|
-
if (this.current.type
|
|
1574
|
+
if (this.current.type === 'RPAREN') break;
|
|
1575
|
+
elements.push(this.expression());
|
|
846
1576
|
}
|
|
847
1577
|
}
|
|
848
1578
|
|
|
1579
|
+
if (this.current.type !== 'RPAREN') {
|
|
1580
|
+
throw new ParseError(
|
|
1581
|
+
"Expected ')' after expression",
|
|
1582
|
+
this.current,
|
|
1583
|
+
this.source
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
849
1587
|
this.eat('RPAREN');
|
|
850
1588
|
|
|
851
|
-
if (this.current.type === 'ARROW')
|
|
1589
|
+
if (this.current.type === 'ARROW') {
|
|
1590
|
+
return this.arrowFunction(elements);
|
|
1591
|
+
}
|
|
852
1592
|
|
|
853
|
-
return elements.length === 1
|
|
1593
|
+
return elements.length === 1
|
|
1594
|
+
? elements[0]
|
|
1595
|
+
: { type: 'ArrayExpression', elements, line: startLine, column: startCol };
|
|
854
1596
|
}
|
|
855
1597
|
|
|
856
1598
|
if (t.type === 'LBRACKET') {
|
|
857
1599
|
const startLine = t.line;
|
|
858
1600
|
const startCol = t.column;
|
|
859
1601
|
this.eat('LBRACKET');
|
|
1602
|
+
|
|
860
1603
|
const elements = [];
|
|
861
1604
|
if (this.current.type !== 'RBRACKET') {
|
|
862
1605
|
elements.push(this.expression());
|
|
863
1606
|
while (this.current.type === 'COMMA') {
|
|
864
1607
|
this.eat('COMMA');
|
|
865
|
-
if (this.current.type
|
|
1608
|
+
if (this.current.type === 'RBRACKET') break;
|
|
1609
|
+
elements.push(this.expression());
|
|
866
1610
|
}
|
|
867
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
|
+
|
|
868
1621
|
this.eat('RBRACKET');
|
|
1622
|
+
|
|
869
1623
|
return { type: 'ArrayExpression', elements, line: startLine, column: startCol };
|
|
870
1624
|
}
|
|
871
1625
|
|
|
872
1626
|
if (t.type === 'LBRACE') {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1627
|
+
const startLine = t.line;
|
|
1628
|
+
const startCol = t.column;
|
|
1629
|
+
this.eat('LBRACE');
|
|
876
1630
|
|
|
877
|
-
|
|
1631
|
+
const props = [];
|
|
878
1632
|
|
|
879
|
-
|
|
880
|
-
|
|
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
|
+
}
|
|
881
1642
|
|
|
882
|
-
|
|
883
|
-
const k = this.current;
|
|
884
|
-
this.eat('IDENTIFIER');
|
|
885
|
-
key = {
|
|
886
|
-
type: 'Literal',
|
|
887
|
-
value: k.value,
|
|
888
|
-
line: k.line,
|
|
889
|
-
column: k.column
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
else if (this.current.type === 'STRING') {
|
|
893
|
-
const k = this.current;
|
|
894
|
-
this.eat('STRING');
|
|
895
|
-
key = {
|
|
896
|
-
type: 'Literal',
|
|
897
|
-
value: k.value,
|
|
898
|
-
line: k.line,
|
|
899
|
-
column: k.column
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
else {
|
|
903
|
-
throw new ParseError(
|
|
904
|
-
'Invalid object key',
|
|
905
|
-
this.current,
|
|
906
|
-
this.source
|
|
907
|
-
);
|
|
908
|
-
}
|
|
1643
|
+
let key;
|
|
909
1644
|
|
|
910
|
-
|
|
911
|
-
|
|
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
|
+
}
|
|
912
1663
|
|
|
913
|
-
|
|
1664
|
+
if (this.current.type !== 'COLON') {
|
|
1665
|
+
throw new ParseError(
|
|
1666
|
+
"Expected ':' after object key",
|
|
1667
|
+
this.current,
|
|
1668
|
+
this.source
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
914
1671
|
|
|
915
|
-
|
|
916
|
-
|
|
1672
|
+
this.eat('COLON');
|
|
1673
|
+
const value = this.expression();
|
|
1674
|
+
props.push({ key, value });
|
|
917
1675
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
1676
|
+
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
1677
|
+
}
|
|
921
1678
|
|
|
1679
|
+
this.eat('RBRACE');
|
|
1680
|
+
return { type: 'ObjectExpression', props, line: startLine, column: startCol };
|
|
1681
|
+
}
|
|
922
1682
|
|
|
923
1683
|
throw new ParseError(
|
|
924
1684
|
`Unexpected token '${t.type}'`,
|
|
@@ -927,7 +1687,6 @@ arrowFunction(params) {
|
|
|
927
1687
|
);
|
|
928
1688
|
}
|
|
929
1689
|
|
|
930
|
-
|
|
931
1690
|
}
|
|
932
1691
|
|
|
933
1692
|
module.exports = Parser;
|