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/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',
@@ -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
- } else {
292
- test = this.expression();
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
- 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
+
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({ 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
+
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
- 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
+
341
715
  } else {
342
- 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
+ );
343
731
  }
344
732
 
345
733
  this.eat('FROM');
734
+
346
735
  const pathToken = this.current;
347
- 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
+
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; // FOR token
759
+ const t = this.current;
363
760
  this.eat('FOR');
364
761
 
365
- if ((this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
366
- (this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')) {
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
- variableLine = this.current.line;
377
- variableColumn = this.current.column;
378
- variable = this.current.value;
379
- this.eat('IDENTIFIER');
380
- this.eat('IN');
381
- iterable = this.expression();
382
- } else {
383
- variableLine = this.current.line;
384
- variableColumn = this.current.column;
385
- variable = this.current.value;
386
- this.eat('IDENTIFIER');
387
- this.eat('IN');
388
- 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
+ );
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' ? this.varDeclaration() : this.expressionStatement();
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') test = this.expression();
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') 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
+
422
860
  this.eat('RPAREN');
423
861
  } else {
424
- init = this.expression();
425
- if (this.current.type === 'IN') {
426
- this.eat('IN');
427
- test = this.expression(); // iterable
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
- const paramToken = this.current;
469
- 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
+ });
470
943
  this.eat('IDENTIFIER');
944
+
471
945
  while (this.current.type === 'COMMA') {
472
946
  this.eat('COMMA');
473
- const paramToken2 = this.current;
474
- 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
+ });
475
963
  this.eat('IDENTIFIER');
476
964
  }
477
965
  }
478
- this.eat('RPAREN');
479
- } else {
480
- if (this.current.type === 'IDENTIFIER') {
481
- const paramToken = this.current;
482
- params.push({ name: paramToken.value, line: paramToken.line, column: paramToken.column });
483
- 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
+ );
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
- 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
+ };
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
- 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
+ };
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
- 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
+ );
544
1096
  }
545
1097
 
546
1098
  if (t.type === 'EQUAL') {
547
1099
  this.eat('EQUAL');
548
1100
  const right = this.assignment();
549
- 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
+ };
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
- 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
+
560
1132
  this.eat('COLON');
561
- const alternate = this.expression();
562
- 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
+ };
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
- if (this.current.type === 'RBRACKET') this.eat('RBRACKET');
685
- 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
+ };
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
- 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
+
695
1327
  args.push(this.expression());
696
- 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
+ );
697
1343
  }
698
- if (this.current.type === 'RPAREN') this.eat('RPAREN');
699
- 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
+ };
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
- const property = this.current.value || '';
708
- if (this.current.type === 'IDENTIFIER') this.eat('IDENTIFIER');
709
- 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
+ };
710
1381
  continue;
711
1382
  }
712
1383
 
713
1384
  if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
714
1385
  this.eat(t.type);
715
- 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
+ };
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
- } else {
736
- 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();
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 !== 'RPAREN') args.push(this.expression());
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 !== 'RPAREN') args.push(this.expression());
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
- return {
816
- type: 'CallExpression',
817
- callee: { type: 'Identifier', name: 'ask', line: t.line, column: t.column },
818
- arguments: args,
819
- line: t.line,
820
- 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
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 !== 'RPAREN') elements.push(this.expression());
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') return this.arrowFunction(elements);
1589
+ if (this.current.type === 'ARROW') {
1590
+ return this.arrowFunction(elements);
1591
+ }
852
1592
 
853
- 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 };
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 !== 'RBRACKET') elements.push(this.expression());
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
- const startLine = t.line;
874
- const startCol = t.column;
875
- this.eat('LBRACE');
1627
+ const startLine = t.line;
1628
+ const startCol = t.column;
1629
+ this.eat('LBRACE');
876
1630
 
877
- const props = [];
1631
+ const props = [];
878
1632
 
879
- while (this.current.type !== 'RBRACE') {
880
- 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
+ }
881
1642
 
882
- if (this.current.type === 'IDENTIFIER') {
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
- this.eat('COLON');
911
- 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
+ }
912
1663
 
913
- 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
+ }
914
1671
 
915
- if (this.current.type === 'COMMA') this.eat('COMMA');
916
- }
1672
+ this.eat('COLON');
1673
+ const value = this.expression();
1674
+ props.push({ key, value });
917
1675
 
918
- this.eat('RBRACE');
919
- return { type: 'ObjectExpression', props, line: startLine, column: startCol };
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;