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/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') this.eat('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
- if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
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') this.eat('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
- this.eat('RPAREN');
227
- } else {
228
- if (this.current.type === 'IDENTIFIER') {
229
- params.push(this.current.value);
230
- this.eat('IDENTIFIER');
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
- this.eat('ELSE');
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
- specifiers.push({ type: 'NamespaceImport', local: name, line: t.line, column: t.column });
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({ type: 'NamedImport', imported: importedName, local: localName, line: importedLine, column: importedColumn });
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
- specifiers.push({ type: 'DefaultImport', local: localName, line: localLine, column: localColumn });
707
+
708
+ specifiers.push({
709
+ type: 'DefaultImport',
710
+ local: localName,
711
+ line: localLine,
712
+ column: localColumn
713
+ });
714
+
375
715
  } else {
376
- throw new Error(`Unexpected token in import at line ${this.current.line}, column ${this.current.column}`);
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
- if (pathToken.type !== 'STRING') throw new Error(`Expected string after FROM at line ${this.current.line}, column ${this.current.column}`);
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; // FOR token
759
+ const t = this.current;
397
760
  this.eat('FOR');
398
761
 
399
- if ((this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
400
- (this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')) {
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
- variableLine = this.current.line;
411
- variableColumn = this.current.column;
412
- variable = this.current.value;
413
- this.eat('IDENTIFIER');
414
- this.eat('IN');
415
- iterable = this.expression();
416
- } else {
417
- variableLine = this.current.line;
418
- variableColumn = this.current.column;
419
- variable = this.current.value;
420
- this.eat('IDENTIFIER');
421
- this.eat('IN');
422
- iterable = this.expression();
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' ? this.varDeclaration() : this.expressionStatement();
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') test = this.expression();
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') update = this.expression();
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
- init = this.expression();
459
- if (this.current.type === 'IN') {
460
- this.eat('IN');
461
- test = this.expression(); // iterable
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
- const paramToken = this.current;
503
- params.push({ name: paramToken.value, line: paramToken.line, column: paramToken.column });
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
- const paramToken2 = this.current;
508
- params.push({ name: paramToken2.value, line: paramToken2.line, column: paramToken2.column });
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
- this.eat('RPAREN');
513
- } else {
514
- if (this.current.type === 'IDENTIFIER') {
515
- const paramToken = this.current;
516
- params.push({ name: paramToken.value, line: paramToken.line, column: paramToken.column });
517
- this.eat('IDENTIFIER');
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
- return { type: 'FunctionDeclaration', name, params, body, line: t.line, column: t.column };
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
- return { type: 'BlockStatement', body, line: t.line, column: t.column };
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
- return { type: 'CompoundAssignment', operator: op, left: node, right, line: t.line, column: t.column };
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
- return { type: 'AssignmentExpression', left: node, right, line: t.line, column: t.column };
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
- const consequent = this.expression();
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
- node = { type: 'ConditionalExpression', test: node, consequent, alternate, line: t.line, column: t.column };
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
- if (this.current.type === 'RBRACKET') this.eat('RBRACKET');
719
- node = { type: 'IndexExpression', object: node, indexer: index, line: startLine, column: startCol };
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
- while (this.current.type !== 'RPAREN' && this.current.type !== 'EOF') {
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
- if (this.current.type === 'COMMA') this.eat('COMMA');
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
- if (this.current.type === 'RPAREN') this.eat('RPAREN');
733
- node = { type: 'CallExpression', callee: node, arguments: args, line: startLine, column: startCol };
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
- const property = this.current.value || '';
742
- if (this.current.type === 'IDENTIFIER') this.eat('IDENTIFIER');
743
- node = { type: 'MemberExpression', object: node, property, line: startLine, column: startCol };
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
- node = { type: 'UpdateExpression', operator: t.type, argument: node, prefix: false, line: t.line, column: t.column };
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
- } else {
770
- body = this.expression();
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 !== 'RPAREN') args.push(this.expression());
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 !== 'RPAREN') args.push(this.expression());
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
- return {
850
- type: 'CallExpression',
851
- callee: { type: 'Identifier', name: 'ask', line: t.line, column: t.column },
852
- arguments: args,
853
- line: t.line,
854
- column: t.column
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 !== 'RPAREN') elements.push(this.expression());
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') return this.arrowFunction(elements);
1589
+ if (this.current.type === 'ARROW') {
1590
+ return this.arrowFunction(elements);
1591
+ }
886
1592
 
887
- return elements.length === 1 ? elements[0] : { type: 'ArrayExpression', elements, line: startLine, column: startCol };
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 !== 'RBRACKET') elements.push(this.expression());
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
- const startLine = t.line;
908
- const startCol = t.column;
909
- this.eat('LBRACE');
1627
+ const startLine = t.line;
1628
+ const startCol = t.column;
1629
+ this.eat('LBRACE');
910
1630
 
911
- const props = [];
1631
+ const props = [];
912
1632
 
913
- while (this.current.type !== 'RBRACE') {
914
- let key;
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
- if (this.current.type === 'IDENTIFIER') {
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
- this.eat('COLON');
945
- const value = this.expression();
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
- props.push({ key, value });
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
- if (this.current.type === 'COMMA') this.eat('COMMA');
950
- }
1672
+ this.eat('COLON');
1673
+ const value = this.expression();
1674
+ props.push({ key, value });
951
1675
 
952
- this.eat('RBRACE');
953
- return { type: 'ObjectExpression', props, line: startLine, column: startCol };
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;