starlight-cli 1.1.11 → 1.1.13

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