starlight-cli 1.1.11 → 1.1.12

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