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/evaluator.js CHANGED
@@ -9,35 +9,77 @@ class ReturnValue {
9
9
  }
10
10
  class BreakSignal {}
11
11
  class ContinueSignal {}
12
+ const COLOR = {
13
+ reset: '\x1b[0m',
14
+ red: '\x1b[31m',
15
+ yellow: '\x1b[33m',
16
+ white: '\x1b[37m'
17
+ };
18
+
12
19
  class RuntimeError extends Error {
13
- constructor(message, node, source) {
14
- const line = node?.line ?? '?';
15
- const column = node?.column ?? '?';
16
-
17
- let output = ` ${message} at line ${line}, column ${column}\n`;
18
-
19
- if (source && node?.line != null) {
20
- const lines = source.split('\n');
21
- const srcLine = lines[node.line - 1] || '';
22
- output += ` ${srcLine}\n`;
23
- const caretPos =
24
- typeof column === 'number' && column > 0
25
- ? column - 1
26
- : 0;
27
-
28
- output += ` ${' '.repeat(caretPos)}^\n`;
29
- ;
20
+ constructor(message, node, source, env = null) {
21
+ const line = node?.line ?? '?';
22
+ const column = node?.column ?? '?';
23
+
24
+ let output =
25
+ `${COLOR.red}${message}${COLOR.reset} at line ${line}, column ${column}\n`;
26
+
27
+ if (source && node?.line != null) {
28
+ const lines = source.split('\n');
29
+ const srcLine = lines[node.line - 1] || '';
30
+ const caretPos =
31
+ typeof column === 'number' && column > 0 ? column - 1 : 0;
32
+
33
+ output += `${COLOR.white} ${srcLine}\n`;
34
+ output += ` ${' '.repeat(caretPos)}^\n${COLOR.reset}`;
35
+ }
36
+
37
+ if (env && message.startsWith('Undefined variable:')) {
38
+ const nameMatch = message.match(/"(.+?)"/);
39
+ if (nameMatch) {
40
+ const name = nameMatch[1];
41
+ const suggestion = RuntimeError.suggest(name, env);
42
+ if (suggestion) {
43
+ output +=
44
+ `${COLOR.yellow}Did you mean "${suggestion}"?${COLOR.reset}\n`;
30
45
  }
46
+ }
47
+ }
48
+
49
+ super(output);
50
+ this.name = 'RuntimeError';
51
+ this.line = line;
52
+ this.column = column;
53
+ }
31
54
 
32
- super(output);
55
+ static suggest(name, env) {
56
+ const names = new Set();
57
+ let current = env;
33
58
 
34
- this.name = 'RuntimeError';
35
- this.line = line;
36
- this.column = column;
59
+ while (current) {
60
+ for (const key of Object.keys(current.store)) {
61
+ names.add(key);
62
+ }
63
+ current = current.parent;
37
64
  }
38
- }
39
65
 
66
+ let best = null;
67
+ let bestScore = Infinity;
68
+
69
+ for (const item of names) {
70
+ const dist =
71
+ Math.abs(item.length - name.length) +
72
+ [...name].filter((c, i) => c !== item[i]).length;
73
+
74
+ if (dist < bestScore && dist <= 2) {
75
+ bestScore = dist;
76
+ best = item;
77
+ }
78
+ }
40
79
 
80
+ return best;
81
+ }
82
+ }
41
83
 
42
84
  class Environment {
43
85
  constructor(parent = null) {
@@ -55,16 +97,7 @@ class Environment {
55
97
  if (name in this.store) return this.store[name];
56
98
  if (this.parent) return this.parent.get(name, node, source);
57
99
 
58
- let suggestion = null;
59
- if (node && source) {
60
- suggestion = this.suggest?.(name, this) || null;
61
- }
62
-
63
- const message = suggestion
64
- ? `Undefined variable: "${name}". Did you mean "${suggestion}"?`
65
- : `Undefined variable: "${name}"`;
66
-
67
- throw new RuntimeError(message, node, source);
100
+ throw new RuntimeError(`Undefined variable: "${name}"`, node, source, this);
68
101
  }
69
102
 
70
103
 
@@ -116,31 +149,7 @@ async callFunction(fn, args, env, node = null) {
116
149
  }
117
150
 
118
151
 
119
- suggest(name, env) {
120
- const names = new Set();
121
- let current = env;
122
- while (current) {
123
- for (const key of Object.keys(current.store)) {
124
- names.add(key);
125
- }
126
- current = current.parent;
127
- }
128
-
129
- let best = null;
130
- let bestScore = Infinity;
131
-
132
- for (const item of names) {
133
- const dist = Math.abs(item.length - name.length) +
134
- [...name].filter((c, i) => c !== item[i]).length;
135
152
 
136
- if (dist < bestScore && dist <= 2) {
137
- bestScore = dist;
138
- best = item;
139
- }
140
- }
141
-
142
- return best;
143
- }
144
153
  formatValue(value, seen = new Set()) {
145
154
  const color = require('starlight-color');
146
155
 
@@ -483,10 +492,17 @@ async evalProgram(node, env) {
483
492
  try {
484
493
  result = await this.evaluate(stmt, env);
485
494
  } catch (e) {
495
+ // Re-throw known runtime control signals
486
496
  if (e instanceof RuntimeError || e instanceof BreakSignal || e instanceof ContinueSignal || e instanceof ReturnValue) {
487
497
  throw e;
488
498
  }
489
- throw new RuntimeError(e.message || 'Error in program', stmt, this.source);
499
+ // Wrap unexpected errors with RuntimeError including env for suggestions
500
+ throw new RuntimeError(
501
+ e.message || 'Error in program',
502
+ stmt,
503
+ this.source,
504
+ env
505
+ );
490
506
  }
491
507
  }
492
508
  return result;
@@ -496,6 +512,7 @@ async evalStartStatement(node, env) {
496
512
  try {
497
513
  const value = await this.evaluate(node.discriminant, env);
498
514
  let executing = false;
515
+
499
516
  for (const c of node.cases) {
500
517
  try {
501
518
  if (!executing) {
@@ -513,11 +530,11 @@ async evalStartStatement(node, env) {
513
530
  caseErr instanceof ContinueSignal) {
514
531
  throw caseErr; // propagate signals
515
532
  }
516
-
517
533
  throw new RuntimeError(
518
534
  caseErr.message || 'Error evaluating case in start statement',
519
535
  c,
520
- this.source
536
+ this.source,
537
+ env
521
538
  );
522
539
  }
523
540
  }
@@ -533,28 +550,31 @@ async evalStartStatement(node, env) {
533
550
  throw new RuntimeError(
534
551
  err.message || 'Error evaluating start statement',
535
552
  node,
536
- this.source
553
+ this.source,
554
+ env
537
555
  );
538
556
  }
539
557
  }
540
558
 
541
-
542
559
  async evalRaceClause(node, env) {
543
560
  try {
544
561
  const testValue = await this.evaluate(node.test, env);
545
562
  const result = await this.evaluate(node.consequent, new Environment(env));
546
563
  return { testValue, result };
547
564
  } catch (err) {
548
- if (err instanceof RuntimeError ||
549
- err instanceof ReturnValue ||
550
- err instanceof BreakSignal ||
551
- err instanceof ContinueSignal) {
565
+ if (
566
+ err instanceof RuntimeError ||
567
+ err instanceof ReturnValue ||
568
+ err instanceof BreakSignal ||
569
+ err instanceof ContinueSignal
570
+ ) {
552
571
  throw err;
553
572
  }
554
573
  throw new RuntimeError(
555
574
  err.message || 'Error evaluating race clause',
556
575
  node,
557
- this.source
576
+ this.source,
577
+ env
558
578
  );
559
579
  }
560
580
  }
@@ -565,7 +585,12 @@ async evalDoTrack(node, env) {
565
585
  } catch (err) {
566
586
  if (!node.handler) {
567
587
  if (err instanceof RuntimeError) throw err;
568
- throw new RuntimeError(err.message || 'Error in doTrack body', node.body, this.source);
588
+ throw new RuntimeError(
589
+ err.message || 'Error in doTrack body',
590
+ node.body,
591
+ this.source,
592
+ env
593
+ );
569
594
  }
570
595
 
571
596
  const trackEnv = new Environment(env);
@@ -575,12 +600,16 @@ async evalDoTrack(node, env) {
575
600
  return await this.evaluate(node.handler, trackEnv);
576
601
  } catch (handlerErr) {
577
602
  if (handlerErr instanceof RuntimeError) throw handlerErr;
578
- throw new RuntimeError(handlerErr.message || 'Error in doTrack handler', node.handler, this.source);
603
+ throw new RuntimeError(
604
+ handlerErr.message || 'Error in doTrack handler',
605
+ node.handler,
606
+ this.source,
607
+ trackEnv
608
+ );
579
609
  }
580
610
  }
581
611
  }
582
612
 
583
-
584
613
  async evalImport(node, env) {
585
614
  const spec = node.path;
586
615
  let lib;
@@ -596,34 +625,51 @@ async evalImport(node, env) {
596
625
  : path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
597
626
 
598
627
  if (!fs.existsSync(fullPath)) {
599
- throw new RuntimeError(`Import not found: ${spec}`, node, this.source);
628
+ throw new RuntimeError(
629
+ `Import not found: ${spec}`,
630
+ node,
631
+ this.source,
632
+ env
633
+ );
600
634
  }
601
635
 
602
- const code = fs.readFileSync(fullPath, 'utf-8');
603
- const tokens = new Lexer(code).getTokens();
604
- const ast = new Parser(tokens).parse();
636
+ try {
637
+ const code = fs.readFileSync(fullPath, 'utf-8');
638
+ const tokens = new Lexer(code).getTokens();
639
+ const ast = new Parser(tokens).parse();
605
640
 
606
- const moduleEnv = new Environment(env);
607
- await this.evaluate(ast, moduleEnv);
641
+ const moduleEnv = new Environment(env);
642
+ await this.evaluate(ast, moduleEnv);
608
643
 
609
- lib = {};
610
- for (const key of Object.keys(moduleEnv.store)) {
611
- lib[key] = moduleEnv.store[key];
612
- }
644
+ lib = {};
645
+ for (const key of Object.keys(moduleEnv.store)) {
646
+ lib[key] = moduleEnv.store[key];
647
+ }
613
648
 
614
- lib.default = lib;
649
+ lib.default = lib;
650
+ } catch (parseErr) {
651
+ throw new RuntimeError(
652
+ parseErr.message || `Failed to import module: ${spec}`,
653
+ node,
654
+ this.source,
655
+ env
656
+ );
657
+ }
615
658
  }
616
659
 
617
660
  for (const imp of node.specifiers) {
618
661
  if (imp.type === 'DefaultImport') {
619
662
  env.define(imp.local, lib.default ?? lib);
620
- }
621
- if (imp.type === 'NamespaceImport') {
663
+ } else if (imp.type === 'NamespaceImport') {
622
664
  env.define(imp.local, lib);
623
- }
624
- if (imp.type === 'NamedImport') {
665
+ } else if (imp.type === 'NamedImport') {
625
666
  if (!(imp.imported in lib)) {
626
- throw new RuntimeError(`Module '${spec}' has no export '${imp.imported}'`, node, this.source);
667
+ throw new RuntimeError(
668
+ `Module '${spec}' has no export '${imp.imported}'`,
669
+ node,
670
+ this.source,
671
+ env
672
+ );
627
673
  }
628
674
  env.define(imp.local, lib[imp.imported]);
629
675
  }
@@ -631,7 +677,6 @@ async evalImport(node, env) {
631
677
 
632
678
  return null;
633
679
  }
634
-
635
680
  async evalBlock(node, env) {
636
681
  let result = null;
637
682
  for (const stmt of node.body) {
@@ -650,32 +695,55 @@ async evalBlock(node, env) {
650
695
  throw new RuntimeError(
651
696
  e.message || 'Error in block',
652
697
  stmt,
653
- this.source
698
+ this.source,
699
+ env // pass env for suggestions
654
700
  );
655
701
  }
656
702
  }
657
703
  return result;
658
704
  }
659
705
 
660
-
661
-
662
706
  async evalVarDeclaration(node, env) {
663
707
  if (!node.expr) {
664
- throw new RuntimeError('Variable declaration requires an initializer', node, this.source);
708
+ throw new RuntimeError(
709
+ 'Variable declaration requires an initializer',
710
+ node,
711
+ this.source,
712
+ env // pass env for suggestions
713
+ );
665
714
  }
666
715
 
667
- const val = await this.evaluate(node.expr, env);
668
- return env.define(node.id, val);
716
+ try {
717
+ const val = await this.evaluate(node.expr, env);
718
+ return env.define(node.id, val);
719
+ } catch (e) {
720
+ if (e instanceof RuntimeError) throw e;
721
+ throw new RuntimeError(
722
+ e.message || 'Error evaluating variable declaration',
723
+ node,
724
+ this.source,
725
+ env
726
+ );
727
+ }
669
728
  }
670
729
 
671
-
672
730
  evalArrowFunction(node, env) {
673
731
  if (!node.body) {
674
- throw new RuntimeError('Arrow function missing body', node, this.source);
732
+ throw new RuntimeError(
733
+ 'Arrow function missing body',
734
+ node,
735
+ this.source,
736
+ env
737
+ );
675
738
  }
676
739
 
677
740
  if (!Array.isArray(node.params)) {
678
- throw new RuntimeError('Invalid arrow function parameters', node, this.source);
741
+ throw new RuntimeError(
742
+ 'Invalid arrow function parameters',
743
+ node,
744
+ this.source,
745
+ env
746
+ );
679
747
  }
680
748
 
681
749
  const evaluator = this;
@@ -683,7 +751,6 @@ evalArrowFunction(node, env) {
683
751
  return async function (...args) {
684
752
  const localEnv = new Environment(env);
685
753
 
686
- // Bind parameters safely
687
754
  node.params.forEach((p, i) => {
688
755
  const paramName = typeof p === 'string' ? p : p.name;
689
756
  localEnv.define(paramName, args[i]);
@@ -691,77 +758,127 @@ evalArrowFunction(node, env) {
691
758
 
692
759
  try {
693
760
  if (node.isBlock) {
694
- // Block body
695
761
  const result = await evaluator.evaluate(node.body, localEnv);
696
- return result === undefined ? null : result; // ensure null instead of undefined
762
+ return result === undefined ? null : result;
697
763
  } else {
698
- // Expression body
699
764
  const result = await evaluator.evaluate(node.body, localEnv);
700
- return result === undefined ? null : result; // ensure null instead of undefined
765
+ return result === undefined ? null : result;
701
766
  }
702
767
  } catch (err) {
703
768
  if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
704
- throw err;
769
+ if (err instanceof RuntimeError) throw err; // preserve RuntimeErrors
770
+ throw new RuntimeError(
771
+ err.message || 'Error evaluating arrow function',
772
+ node,
773
+ evaluator.source,
774
+ localEnv
775
+ );
705
776
  }
706
777
  };
707
778
  }
708
779
 
709
-
710
-
711
780
  async evalAssignment(node, env) {
712
781
  const rightVal = await this.evaluate(node.right, env);
713
782
  const left = node.left;
714
783
 
715
- if (left.type === 'Identifier') return env.set(left.name, rightVal);
784
+ try {
785
+ if (left.type === 'Identifier') return env.set(left.name, rightVal);
786
+
787
+ if (left.type === 'MemberExpression') {
788
+ const obj = await this.evaluate(left.object, env);
789
+ if (obj == null) throw new RuntimeError(
790
+ 'Cannot assign to null or undefined',
791
+ node,
792
+ this.source,
793
+ env
794
+ );
795
+ obj[left.property] = rightVal; // dynamic creation allowed
796
+ return rightVal;
797
+ }
716
798
 
717
- if (left.type === 'MemberExpression') {
718
- const obj = await this.evaluate(left.object, env);
719
- if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
720
- obj[left.property] = rightVal; // dynamic creation of new properties allowed
721
- return rightVal;
722
- }
799
+ if (left.type === 'IndexExpression') {
800
+ const obj = await this.evaluate(left.object, env);
801
+ const idx = await this.evaluate(left.indexer, env);
802
+ if (obj == null) throw new RuntimeError(
803
+ 'Cannot assign to null or undefined',
804
+ node,
805
+ this.source,
806
+ env
807
+ );
808
+ obj[idx] = rightVal; // dynamic creation allowed
809
+ return rightVal;
810
+ }
723
811
 
724
- if (left.type === 'IndexExpression') {
725
- const obj = await this.evaluate(left.object, env);
726
- const idx = await this.evaluate(left.indexer, env);
727
- if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
728
- obj[idx] = rightVal; // dynamic creation allowed
729
- return rightVal;
730
- }
812
+ throw new RuntimeError(
813
+ 'Invalid assignment target',
814
+ node,
815
+ this.source,
816
+ env
817
+ );
731
818
 
732
- throw new RuntimeError('Invalid assignment target', node, this.source);
819
+ } catch (e) {
820
+ if (e instanceof RuntimeError) throw e;
821
+ throw new RuntimeError(
822
+ e.message || 'Error in assignment',
823
+ node,
824
+ this.source,
825
+ env
826
+ );
827
+ }
733
828
  }
734
829
 
735
830
  async evalCompoundAssignment(node, env) {
736
831
  const left = node.left;
737
832
  let current;
738
833
 
739
- if (left.type === 'Identifier') current = env.get(left.name, left, this.source);
740
- else if (left.type === 'MemberExpression') current = await this.evalMember(left, env) ?? 0;
741
- else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env) ?? 0;
742
- else throw new RuntimeError('Invalid compound assignment target', node, this.source);
743
-
744
- const rhs = await this.evaluate(node.right, env);
745
- let computed;
746
-
747
- switch (node.operator) {
748
- case 'PLUSEQ': computed = current + rhs; break;
749
- case 'MINUSEQ': computed = current - rhs; break;
750
- case 'STAREQ': computed = current * rhs; break;
751
- case 'SLASHEQ': computed = current / rhs; break;
752
- case 'MODEQ': computed = current % rhs; break;
753
- default: throw new RuntimeError(`Unknown compound operator: ${node.operator}`, node, this.source);
754
- }
834
+ try {
835
+ if (left.type === 'Identifier') current = env.get(left.name, left, this.source);
836
+ else if (left.type === 'MemberExpression') current = await this.evalMember(left, env) ?? 0;
837
+ else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env) ?? 0;
838
+ else throw new RuntimeError(
839
+ 'Invalid compound assignment target',
840
+ node,
841
+ this.source,
842
+ env
843
+ );
755
844
 
756
- if (left.type === 'Identifier') env.set(left.name, computed);
757
- else if (left.type === 'MemberExpression')
758
- await this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
759
- else
760
- await this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
845
+ const rhs = await this.evaluate(node.right, env);
846
+ let computed;
847
+
848
+ switch (node.operator) {
849
+ case 'PLUSEQ': computed = current + rhs; break;
850
+ case 'MINUSEQ': computed = current - rhs; break;
851
+ case 'STAREQ': computed = current * rhs; break;
852
+ case 'SLASHEQ': computed = current / rhs; break;
853
+ case 'MODEQ': computed = current % rhs; break;
854
+ default: throw new RuntimeError(
855
+ `Unknown compound operator: ${node.operator}`,
856
+ node,
857
+ this.source,
858
+ env
859
+ );
860
+ }
761
861
 
762
- return computed;
763
- }
862
+ if (left.type === 'Identifier') env.set(left.name, computed);
863
+ else if (left.type === 'MemberExpression' || left.type === 'IndexExpression') {
864
+ await this.evalAssignment(
865
+ { left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' },
866
+ env
867
+ );
868
+ }
869
+
870
+ return computed;
764
871
 
872
+ } catch (e) {
873
+ if (e instanceof RuntimeError) throw e;
874
+ throw new RuntimeError(
875
+ e.message || 'Error in compound assignment',
876
+ node,
877
+ this.source,
878
+ env
879
+ );
880
+ }
881
+ }
765
882
 
766
883
  async evalSldeploy(node, env) {
767
884
  const val = await this.evaluate(node.expr, env);
@@ -770,108 +887,162 @@ async evalSldeploy(node, env) {
770
887
  }
771
888
 
772
889
 
773
-
774
890
  async evalAsk(node, env) {
775
- const prompt = await this.evaluate(node.prompt, env);
776
-
777
- if (typeof prompt !== 'string') {
778
- throw new RuntimeError('ask() prompt must be a string', node, this.source);
779
- }
891
+ try {
892
+ const prompt = await this.evaluate(node.prompt, env);
780
893
 
781
- const input = readlineSync.question(prompt + ' ');
782
- return input;
783
- }
894
+ if (typeof prompt !== 'string') {
895
+ throw new RuntimeError('ask() prompt must be a string', node, this.source, env);
896
+ }
784
897
 
898
+ const input = readlineSync.question(prompt + ' ');
899
+ return input;
785
900
 
786
- async evalDefine(node, env) {
787
- if (!node.id || typeof node.id !== 'string') {
788
- throw new RuntimeError('Invalid identifier in define statement', node, this.source);
901
+ } catch (e) {
902
+ if (e instanceof RuntimeError) throw e;
903
+ throw new RuntimeError(
904
+ e.message || 'Error evaluating ask()',
905
+ node,
906
+ this.source,
907
+ env
908
+ );
789
909
  }
790
-
791
- const val = node.expr ? await this.evaluate(node.expr, env) : null;
792
- return env.define(node.id, val);
793
-
794
910
  }
795
911
 
912
+ async evalDefine(node, env) {
913
+ try {
914
+ if (!node.id || typeof node.id !== 'string') {
915
+ throw new RuntimeError('Invalid identifier in define statement', node, this.source, env);
916
+ }
796
917
 
797
- async evalBinary(node, env) {
798
- const l = await this.evaluate(node.left, env);
799
- const r = await this.evaluate(node.right, env);
800
-
801
- if (node.operator === 'SLASH' && r === 0) {
802
- throw new RuntimeError('Division by zero', node, this.source);
803
- }
804
-
805
- switch (node.operator) {
806
- case 'PLUS': {
807
- if (Array.isArray(l) && Array.isArray(r)) {
808
- return l.concat(r);
809
- }
918
+ const val = node.expr ? await this.evaluate(node.expr, env) : null;
919
+ return env.define(node.id, val);
810
920
 
811
- if (typeof l === 'string' || typeof r === 'string') {
812
- return String(l) + String(r);
921
+ } catch (e) {
922
+ if (e instanceof RuntimeError) throw e;
923
+ throw new RuntimeError(
924
+ e.message || 'Error in define statement',
925
+ node,
926
+ this.source,
927
+ env
928
+ );
813
929
  }
930
+ }
814
931
 
815
- if (typeof l === 'number' && typeof r === 'number') {
816
- return l + r;
817
- }
932
+ async evalBinary(node, env) {
933
+ try {
934
+ const l = await this.evaluate(node.left, env);
935
+ const r = await this.evaluate(node.right, env);
818
936
 
819
- if (typeof l === 'object' && typeof r === 'object') {
820
- return { ...l, ...r };
821
- }
937
+ if (node.operator === 'SLASH' && r === 0) {
938
+ throw new RuntimeError('Division by zero', node, this.source, env);
939
+ }
822
940
 
823
- throw new RuntimeError(
824
- `Unsupported operands for +: ${typeof l} and ${typeof r}`,
825
- node,
826
- this.source
827
- );
828
- }
941
+ switch (node.operator) {
942
+ case 'PLUS': {
943
+ if (Array.isArray(l) && Array.isArray(r)) return l.concat(r);
944
+ if (typeof l === 'string' || typeof r === 'string') return String(l) + String(r);
945
+ if (typeof l === 'number' && typeof r === 'number') return l + r;
946
+ if (typeof l === 'object' && typeof r === 'object') return { ...l, ...r };
829
947
 
830
- case 'MINUS': return l - r;
831
- case 'STAR': return l * r;
832
- case 'SLASH': return l / r;
833
- case 'MOD': return l % r;
834
- case 'EQEQ': return l === r;
835
- case 'NOTEQ': return l !== r;
836
- case 'LT': return l < r;
837
- case 'LTE': return l <= r;
838
- case 'GT': return l > r;
839
- case 'GTE': return l >= r;
840
- default: throw new RuntimeError(`Unknown binary operator: ${node.operator}`, node, this.source);
948
+ throw new RuntimeError(
949
+ `Unsupported operands for +: ${typeof l} and ${typeof r}`,
950
+ node,
951
+ this.source,
952
+ env
953
+ );
954
+ }
955
+ case 'MINUS': return l - r;
956
+ case 'STAR': return l * r;
957
+ case 'SLASH': return l / r;
958
+ case 'MOD': return l % r;
959
+ case 'EQEQ': return l === r;
960
+ case 'NOTEQ': return l !== r;
961
+ case 'LT': return l < r;
962
+ case 'LTE': return l <= r;
963
+ case 'GT': return l > r;
964
+ case 'GTE': return l >= r;
965
+ default:
966
+ throw new RuntimeError(
967
+ `Unknown binary operator: ${node.operator}`,
968
+ node,
969
+ this.source,
970
+ env
971
+ );
972
+ }
841
973
 
974
+ } catch (e) {
975
+ if (e instanceof RuntimeError) throw e;
976
+ throw new RuntimeError(
977
+ e.message || 'Error evaluating binary expression',
978
+ node,
979
+ this.source,
980
+ env
981
+ );
842
982
  }
843
983
  }
844
-
845
984
  async evalLogical(node, env) {
846
- const l = await this.evaluate(node.left, env);
847
-
848
- switch(node.operator) {
849
- case 'AND': return l && await this.evaluate(node.right, env);
850
- case 'OR': return l || await this.evaluate(node.right, env);
851
- case '??': {
852
- const r = await this.evaluate(node.right, env);
853
- return (l !== null && l !== undefined) ? l : r;
985
+ try {
986
+ const l = await this.evaluate(node.left, env);
987
+
988
+ switch (node.operator) {
989
+ case 'AND': return l && await this.evaluate(node.right, env);
990
+ case 'OR': return l || await this.evaluate(node.right, env);
991
+ case '??': {
992
+ const r = await this.evaluate(node.right, env);
993
+ return (l !== null && l !== undefined) ? l : r;
994
+ }
995
+ default:
996
+ throw new RuntimeError(
997
+ `Unknown logical operator: ${node.operator}`,
998
+ node,
999
+ this.source,
1000
+ env
1001
+ );
854
1002
  }
855
- default:
856
- throw new RuntimeError(`Unknown logical operator: ${node.operator}`, node, this.source);
1003
+
1004
+ } catch (e) {
1005
+ if (e instanceof RuntimeError) throw e;
1006
+ throw new RuntimeError(
1007
+ e.message || 'Error evaluating logical expression',
1008
+ node,
1009
+ this.source,
1010
+ env
1011
+ );
857
1012
  }
858
1013
  }
859
1014
 
860
-
861
1015
  async evalUnary(node, env) {
862
- const val = await this.evaluate(node.argument, env);
863
- switch (node.operator) {
864
- case 'NOT': return !val;
865
- case 'MINUS': return -val;
866
- case 'PLUS': return +val;
867
- default: throw new RuntimeError(`Unknown unary operator: ${node.operator}`, node, this.source);
1016
+ try {
1017
+ const val = await this.evaluate(node.argument, env);
1018
+
1019
+ switch (node.operator) {
1020
+ case 'NOT': return !val;
1021
+ case 'MINUS': return -val;
1022
+ case 'PLUS': return +val;
1023
+ default:
1024
+ throw new RuntimeError(
1025
+ `Unknown unary operator: ${node.operator}`,
1026
+ node,
1027
+ this.source,
1028
+ env
1029
+ );
1030
+ }
868
1031
 
1032
+ } catch (e) {
1033
+ if (e instanceof RuntimeError) throw e;
1034
+ throw new RuntimeError(
1035
+ e.message || 'Error evaluating unary expression',
1036
+ node,
1037
+ this.source,
1038
+ env
1039
+ );
869
1040
  }
870
1041
  }
1042
+
871
1043
  async evalIf(node, env) {
872
1044
  let test = await this.evaluate(node.test, env);
873
-
874
- test = !!test;
1045
+ test = !!test; // coerce to boolean
875
1046
 
876
1047
  if (test) {
877
1048
  return await this.evaluate(node.consequent, env);
@@ -885,183 +1056,270 @@ async evalIf(node, env) {
885
1056
  }
886
1057
 
887
1058
  async evalWhile(node, env) {
888
- while (true) {
889
- let test = await this.evaluate(node.test, env);
890
-
891
- test = !!test;
1059
+ try {
1060
+ while (true) {
1061
+ let test = await this.evaluate(node.test, env);
1062
+ test = !!test;
892
1063
 
893
- if (!test) break;
1064
+ if (!test) break;
894
1065
 
895
- try {
896
- await this.evaluate(node.body, env);
897
- } catch (e) {
898
- if (e instanceof BreakSignal) break;
899
- if (e instanceof ContinueSignal) continue;
900
- throw e;
1066
+ try {
1067
+ await this.evaluate(node.body, env);
1068
+ } catch (e) {
1069
+ if (e instanceof BreakSignal) break;
1070
+ if (e instanceof ContinueSignal) continue;
1071
+ throw e;
1072
+ }
901
1073
  }
902
- }
903
1074
 
904
- return null;
905
- }
1075
+ return null;
906
1076
 
1077
+ } catch (e) {
1078
+ if (e instanceof RuntimeError || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
1079
+ throw new RuntimeError(
1080
+ e.message || 'Error evaluating while loop',
1081
+ node,
1082
+ this.source,
1083
+ env
1084
+ );
1085
+ }
1086
+ }
907
1087
  async evalFor(node, env) {
908
- if (node.type === 'ForInStatement') {
909
- const iterable = await this.evaluate(node.iterable, env);
1088
+ try {
1089
+ // ForInStatement
1090
+ if (node.type === 'ForInStatement') {
1091
+ const iterable = await this.evaluate(node.iterable, env);
910
1092
 
911
- if (iterable == null || typeof iterable !== 'object') {
912
- throw new RuntimeError('Cannot iterate over non-iterable', node, this.source);
913
- }
1093
+ if (iterable == null || typeof iterable !== 'object') {
1094
+ throw new RuntimeError(
1095
+ 'Cannot iterate over non-iterable',
1096
+ node,
1097
+ this.source,
1098
+ env
1099
+ );
1100
+ }
914
1101
 
915
- const loopVar = node.variable; // string name of the loop variable
916
- const createLoopEnv = () => node.letKeyword ? new Environment(env) : env;
917
- if (Array.isArray(iterable)) {
918
- for (const value of iterable) {
919
- const loopEnv = createLoopEnv();
920
- loopEnv.define(loopVar, value);
921
-
922
- try {
923
- await this.evaluate(node.body, loopEnv);
924
- } catch (e) {
925
- if (e instanceof BreakSignal) break;
926
- if (e instanceof ContinueSignal) continue;
927
- throw e;
1102
+ const loopVar = node.variable; // string name of the loop variable
1103
+ const createLoopEnv = () => node.letKeyword ? new Environment(env) : env;
1104
+
1105
+ if (Array.isArray(iterable)) {
1106
+ for (const value of iterable) {
1107
+ const loopEnv = createLoopEnv();
1108
+ loopEnv.define(loopVar, value);
1109
+
1110
+ try {
1111
+ await this.evaluate(node.body, loopEnv);
1112
+ } catch (e) {
1113
+ if (e instanceof BreakSignal) break;
1114
+ if (e instanceof ContinueSignal) continue;
1115
+ throw e;
1116
+ }
928
1117
  }
929
- }
930
- }
931
- else {
932
- for (const key of Object.keys(iterable)) {
933
- const loopEnv = createLoopEnv();
934
- loopEnv.define(loopVar, key);
935
-
936
- try {
937
- await this.evaluate(node.body, loopEnv);
938
- } catch (e) {
939
- if (e instanceof BreakSignal) break;
940
- if (e instanceof ContinueSignal) continue;
941
- throw e;
1118
+ } else {
1119
+ for (const key of Object.keys(iterable)) {
1120
+ const loopEnv = createLoopEnv();
1121
+ loopEnv.define(loopVar, key);
1122
+
1123
+ try {
1124
+ await this.evaluate(node.body, loopEnv);
1125
+ } catch (e) {
1126
+ if (e instanceof BreakSignal) break;
1127
+ if (e instanceof ContinueSignal) continue;
1128
+ throw e;
1129
+ }
942
1130
  }
943
1131
  }
944
- }
945
1132
 
946
- return null;
947
- }
1133
+ return null;
1134
+ }
948
1135
 
949
- const local = new Environment(env);
1136
+ // Standard for loop
1137
+ const local = new Environment(env);
950
1138
 
951
- if (node.init) await this.evaluate(node.init, local);
1139
+ if (node.init) await this.evaluate(node.init, local);
952
1140
 
953
- while (!node.test || await this.evaluate(node.test, local)) {
954
- try {
955
- await this.evaluate(node.body, local);
956
- } catch (e) {
957
- if (e instanceof BreakSignal) break;
958
- if (e instanceof ContinueSignal) {
959
- if (node.update) await this.evaluate(node.update, local);
960
- continue;
1141
+ while (!node.test || await this.evaluate(node.test, local)) {
1142
+ try {
1143
+ await this.evaluate(node.body, local);
1144
+ } catch (e) {
1145
+ if (e instanceof BreakSignal) break;
1146
+ if (e instanceof ContinueSignal) {
1147
+ if (node.update) await this.evaluate(node.update, local);
1148
+ continue;
1149
+ }
1150
+ throw e;
961
1151
  }
962
- throw e;
1152
+
1153
+ if (node.update) await this.evaluate(node.update, local);
963
1154
  }
964
1155
 
965
- if (node.update) await this.evaluate(node.update, local);
966
- }
1156
+ return null;
967
1157
 
968
- return null;
1158
+ } catch (err) {
1159
+ if (err instanceof RuntimeError || err instanceof BreakSignal || err instanceof ContinueSignal) throw err;
1160
+ throw new RuntimeError(
1161
+ err.message || 'Error evaluating for loop',
1162
+ node,
1163
+ this.source,
1164
+ env
1165
+ );
1166
+ }
969
1167
  }
970
1168
 
971
1169
  evalFunctionDeclaration(node, env) {
972
- if (!node.name || typeof node.name !== 'string') {
973
- throw new RuntimeError('Function declaration requires a valid name', node, this.source);
974
- }
1170
+ try {
1171
+ if (!node.name || typeof node.name !== 'string') {
1172
+ throw new RuntimeError('Function declaration requires a valid name', node, this.source, env);
1173
+ }
975
1174
 
976
- if (!Array.isArray(node.params)) {
977
- throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source);
978
- }
1175
+ if (!Array.isArray(node.params)) {
1176
+ throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source, env);
1177
+ }
979
1178
 
980
- if (!node.body) {
981
- throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source);
982
- }
1179
+ if (!node.body) {
1180
+ throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source, env);
1181
+ }
983
1182
 
984
- const fn = {
985
- params: node.params,
986
- body: node.body,
987
- env,
988
- async: node.async || false
989
- };
1183
+ const fn = {
1184
+ params: node.params,
1185
+ body: node.body,
1186
+ env,
1187
+ async: node.async || false
1188
+ };
990
1189
 
991
- env.define(node.name, fn);
992
- return null;
993
- }
1190
+ env.define(node.name, fn);
1191
+ return null;
994
1192
 
995
- async evalCall(node, env) {
996
- const calleeEvaluated = await this.evaluate(node.callee, env);
997
- if (typeof calleeEvaluated === 'function') {
998
- const args = [];
999
- for (const a of node.arguments) args.push(await this.evaluate(a, env));
1000
- return await calleeEvaluated(...args);
1001
- }
1002
- if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
1003
- throw new RuntimeError('Call to non-function', node, this.source);
1193
+ } catch (err) {
1194
+ if (err instanceof RuntimeError) throw err;
1195
+ throw new RuntimeError(
1196
+ err.message || 'Error defining function',
1197
+ node,
1198
+ this.source,
1199
+ env
1200
+ );
1004
1201
  }
1202
+ }
1203
+ async evalCall(node, env) {
1204
+ try {
1205
+ const calleeEvaluated = await this.evaluate(node.callee, env);
1005
1206
 
1006
- const fn = calleeEvaluated;
1007
- const callEnv = new Environment(fn.env);
1207
+ // Native JS function
1208
+ if (typeof calleeEvaluated === 'function') {
1209
+ const args = [];
1210
+ for (const a of node.arguments) args.push(await this.evaluate(a, env));
1211
+ return await calleeEvaluated(...args);
1212
+ }
1008
1213
 
1009
- for (let i = 0; i < fn.params.length; i++) {
1010
- const argVal = node.arguments[i]
1011
- ? await this.evaluate(node.arguments[i], env)
1012
- : null;
1214
+ // Not a callable object
1215
+ if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
1216
+ throw new RuntimeError(
1217
+ 'Call to non-function',
1218
+ node,
1219
+ this.source,
1220
+ env
1221
+ );
1222
+ }
1013
1223
 
1014
- const param = fn.params[i];
1015
- const paramName = typeof param === 'string' ? param : param.name;
1224
+ const fn = calleeEvaluated;
1225
+ const callEnv = new Environment(fn.env);
1016
1226
 
1017
- callEnv.define(paramName, argVal);
1018
- }
1227
+ for (let i = 0; i < fn.params.length; i++) {
1228
+ const argVal = node.arguments[i]
1229
+ ? await this.evaluate(node.arguments[i], env)
1230
+ : null;
1019
1231
 
1020
- try {
1021
- const result = await this.evaluate(fn.body, callEnv);
1022
- return result;
1023
- } catch (e) {
1024
- if (e instanceof ReturnValue) return e.value;
1025
- throw e;
1232
+ const param = fn.params[i];
1233
+ const paramName = typeof param === 'string' ? param : param.name;
1234
+ callEnv.define(paramName, argVal);
1235
+ }
1236
+
1237
+ try {
1238
+ const result = await this.evaluate(fn.body, callEnv);
1239
+ return result === undefined ? null : result; // enforce null instead of undefined
1240
+ } catch (e) {
1241
+ if (e instanceof ReturnValue) return e.value === undefined ? null : e.value;
1242
+ throw e;
1243
+ }
1244
+
1245
+ } catch (err) {
1246
+ if (err instanceof RuntimeError) throw err;
1247
+ throw new RuntimeError(
1248
+ err.message || 'Error during function call',
1249
+ node,
1250
+ this.source,
1251
+ env
1252
+ );
1026
1253
  }
1027
1254
  }
1028
1255
 
1029
1256
  async evalIndex(node, env) {
1030
- const obj = await this.evaluate(node.object, env);
1031
- const idx = await this.evaluate(node.indexer, env);
1032
-
1033
- if (obj == null) throw new RuntimeError('Cannot index null or undefined', node, this.source);
1034
- if (Array.isArray(obj)) {
1035
- if (idx < 0 || idx >= obj.length) return undefined;
1036
- return obj[idx];
1037
- }
1038
- if (typeof obj === 'object') {
1039
- return obj[idx]; // undefined if missing
1040
- }
1257
+ try {
1258
+ const obj = await this.evaluate(node.object, env);
1259
+ const idx = await this.evaluate(node.indexer, env);
1041
1260
 
1042
- return undefined;
1043
- }
1044
- async evalObject(node, env) {
1045
- const out = {};
1046
- for (const p of node.props) {
1047
- if (!p.key) {
1048
- throw new RuntimeError('Object property must have a key', node, this.source);
1261
+ if (obj == null) {
1262
+ throw new RuntimeError(
1263
+ 'Cannot index null or undefined',
1264
+ node,
1265
+ this.source,
1266
+ env
1267
+ );
1049
1268
  }
1050
1269
 
1051
- const key = await this.evaluate(p.key, env);
1052
- let value = null;
1053
-
1054
- if (p.value) {
1055
- value = await this.evaluate(p.value, env);
1056
- if (value === undefined) value = null; // <- force null instead of undefined
1270
+ if (Array.isArray(obj)) {
1271
+ if (idx < 0 || idx >= obj.length) return undefined;
1272
+ return obj[idx];
1057
1273
  }
1058
1274
 
1059
- out[key] = value;
1275
+ if (typeof obj === 'object') return obj[idx]; // undefined if missing
1276
+
1277
+ return undefined;
1278
+ } catch (err) {
1279
+ if (err instanceof RuntimeError) throw err;
1280
+ throw new RuntimeError(
1281
+ err.message || 'Error during index access',
1282
+ node,
1283
+ this.source,
1284
+ env
1285
+ );
1060
1286
  }
1061
- return out;
1062
1287
  }
1063
1288
 
1289
+ async evalObject(node, env) {
1290
+ try {
1291
+ const out = {};
1292
+ for (const p of node.props) {
1293
+ if (!p.key) {
1294
+ throw new RuntimeError(
1295
+ 'Object property must have a key',
1296
+ node,
1297
+ this.source,
1298
+ env
1299
+ );
1300
+ }
1301
+
1302
+ const key = await this.evaluate(p.key, env);
1303
+ let value = null;
1064
1304
 
1305
+ if (p.value) {
1306
+ value = await this.evaluate(p.value, env);
1307
+ if (value === undefined) value = null; // force null instead of undefined
1308
+ }
1309
+
1310
+ out[key] = value;
1311
+ }
1312
+ return out;
1313
+ } catch (err) {
1314
+ if (err instanceof RuntimeError) throw err;
1315
+ throw new RuntimeError(
1316
+ err.message || 'Error evaluating object literal',
1317
+ node,
1318
+ this.source,
1319
+ env
1320
+ );
1321
+ }
1322
+ }
1065
1323
 
1066
1324
 
1067
1325
  async evalMember(node, env) {
@@ -1080,33 +1338,61 @@ async evalMember(node, env) {
1080
1338
  return prop;
1081
1339
  }
1082
1340
 
1083
-
1084
1341
  async evalUpdate(node, env) {
1085
1342
  const arg = node.argument;
1086
- const getCurrent = async () => {
1087
- if (arg.type === 'Identifier') return env.get(arg.name, arg, this.source);
1088
- if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
1089
- if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
1090
- throw new RuntimeError('Invalid update target', node, this.source);
1091
1343
 
1344
+ const getCurrent = async () => {
1345
+ try {
1346
+ if (arg.type === 'Identifier') return env.get(arg.name, arg, this.source);
1347
+ if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
1348
+ if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
1349
+ throw new RuntimeError('Invalid update target', node, this.source, env);
1350
+ } catch (err) {
1351
+ if (err instanceof RuntimeError) throw err;
1352
+ throw new RuntimeError(
1353
+ err.message || 'Error accessing update target',
1354
+ node,
1355
+ this.source,
1356
+ env
1357
+ );
1358
+ }
1092
1359
  };
1360
+
1093
1361
  const setValue = async (v) => {
1094
- if (arg.type === 'Identifier') env.set(arg.name, v);
1095
- else if (arg.type === 'MemberExpression') {
1096
- const obj = await this.evaluate(arg.object, env);
1097
- obj[arg.property] = v;
1098
- } else if (arg.type === 'IndexExpression') {
1099
- const obj = await this.evaluate(arg.object, env);
1100
- const idx = await this.evaluate(arg.indexer, env);
1101
- obj[idx] = v;
1362
+ try {
1363
+ if (arg.type === 'Identifier') {
1364
+ env.set(arg.name, v);
1365
+ } else if (arg.type === 'MemberExpression') {
1366
+ const obj = await this.evaluate(arg.object, env);
1367
+ if (obj == null) throw new RuntimeError('Cannot update property of null or undefined', node, this.source, env);
1368
+ obj[arg.property] = v;
1369
+ } else if (arg.type === 'IndexExpression') {
1370
+ const obj = await this.evaluate(arg.object, env);
1371
+ const idx = await this.evaluate(arg.indexer, env);
1372
+ if (obj == null) throw new RuntimeError('Cannot update index of null or undefined', node, this.source, env);
1373
+ obj[idx] = v;
1374
+ }
1375
+ } catch (err) {
1376
+ if (err instanceof RuntimeError) throw err;
1377
+ throw new RuntimeError(
1378
+ err.message || 'Error setting update target',
1379
+ node,
1380
+ this.source,
1381
+ env
1382
+ );
1102
1383
  }
1103
1384
  };
1104
1385
 
1105
1386
  const current = await getCurrent();
1106
- const newVal = (node.operator === 'PLUSPLUS') ? current + 1 : current - 1;
1387
+ const newVal = node.operator === 'PLUSPLUS' ? current + 1 : current - 1;
1107
1388
 
1108
- if (node.prefix) { await setValue(newVal); return newVal; }
1109
- else { await setValue(newVal); return current; }
1389
+ if (node.prefix) {
1390
+ await setValue(newVal);
1391
+ return newVal;
1392
+ } else {
1393
+ await setValue(newVal);
1394
+ return current;
1395
+ }
1110
1396
  }
1111
1397
 
1112
1398
  }