starlight-cli 1.1.10 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/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
-
136
- if (dist < bestScore && dist <= 2) {
137
- bestScore = dist;
138
- best = item;
139
- }
140
- }
141
141
 
142
- return best;
143
- }
144
142
  formatValue(value, seen = new Set()) {
145
143
  const color = require('starlight-color');
146
144
 
@@ -201,6 +199,20 @@ formatValue(value, seen = new Set()) {
201
199
  if (Array.isArray(arg)) return 'array';
202
200
  return typeof arg;
203
201
  });
202
+ this.global.define('isNaN', arg => {
203
+ return typeof arg !== 'number' || Number.isNaN(arg);
204
+ });
205
+ this.global.define('random', (min, max) => {
206
+ if (max === undefined) {
207
+ // Only one argument → random between 0 and min
208
+ return Math.floor(Math.random() * min);
209
+ }
210
+ min = Number(min);
211
+ max = Number(max);
212
+ if (isNaN(min) || isNaN(max)) return 0;
213
+ return Math.floor(Math.random() * (max - min)) + min;
214
+ });
215
+
204
216
  this.global.define('map', async (array, fn) => {
205
217
  if (!Array.isArray(array)) {
206
218
  throw new RuntimeError('map() expects an array', null, evaluator.source);
@@ -469,10 +481,17 @@ async evalProgram(node, env) {
469
481
  try {
470
482
  result = await this.evaluate(stmt, env);
471
483
  } catch (e) {
484
+ // Re-throw known runtime control signals
472
485
  if (e instanceof RuntimeError || e instanceof BreakSignal || e instanceof ContinueSignal || e instanceof ReturnValue) {
473
486
  throw e;
474
487
  }
475
- 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
+ );
476
495
  }
477
496
  }
478
497
  return result;
@@ -482,6 +501,7 @@ async evalStartStatement(node, env) {
482
501
  try {
483
502
  const value = await this.evaluate(node.discriminant, env);
484
503
  let executing = false;
504
+
485
505
  for (const c of node.cases) {
486
506
  try {
487
507
  if (!executing) {
@@ -499,11 +519,11 @@ async evalStartStatement(node, env) {
499
519
  caseErr instanceof ContinueSignal) {
500
520
  throw caseErr; // propagate signals
501
521
  }
502
-
503
522
  throw new RuntimeError(
504
523
  caseErr.message || 'Error evaluating case in start statement',
505
524
  c,
506
- this.source
525
+ this.source,
526
+ env
507
527
  );
508
528
  }
509
529
  }
@@ -519,28 +539,31 @@ async evalStartStatement(node, env) {
519
539
  throw new RuntimeError(
520
540
  err.message || 'Error evaluating start statement',
521
541
  node,
522
- this.source
542
+ this.source,
543
+ env
523
544
  );
524
545
  }
525
546
  }
526
547
 
527
-
528
548
  async evalRaceClause(node, env) {
529
549
  try {
530
550
  const testValue = await this.evaluate(node.test, env);
531
551
  const result = await this.evaluate(node.consequent, new Environment(env));
532
552
  return { testValue, result };
533
553
  } catch (err) {
534
- if (err instanceof RuntimeError ||
535
- err instanceof ReturnValue ||
536
- err instanceof BreakSignal ||
537
- err instanceof ContinueSignal) {
554
+ if (
555
+ err instanceof RuntimeError ||
556
+ err instanceof ReturnValue ||
557
+ err instanceof BreakSignal ||
558
+ err instanceof ContinueSignal
559
+ ) {
538
560
  throw err;
539
561
  }
540
562
  throw new RuntimeError(
541
563
  err.message || 'Error evaluating race clause',
542
564
  node,
543
- this.source
565
+ this.source,
566
+ env
544
567
  );
545
568
  }
546
569
  }
@@ -551,7 +574,12 @@ async evalDoTrack(node, env) {
551
574
  } catch (err) {
552
575
  if (!node.handler) {
553
576
  if (err instanceof RuntimeError) throw err;
554
- 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
+ );
555
583
  }
556
584
 
557
585
  const trackEnv = new Environment(env);
@@ -561,12 +589,16 @@ async evalDoTrack(node, env) {
561
589
  return await this.evaluate(node.handler, trackEnv);
562
590
  } catch (handlerErr) {
563
591
  if (handlerErr instanceof RuntimeError) throw handlerErr;
564
- 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
+ );
565
598
  }
566
599
  }
567
600
  }
568
601
 
569
-
570
602
  async evalImport(node, env) {
571
603
  const spec = node.path;
572
604
  let lib;
@@ -582,34 +614,51 @@ async evalImport(node, env) {
582
614
  : path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
583
615
 
584
616
  if (!fs.existsSync(fullPath)) {
585
- 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
+ );
586
623
  }
587
624
 
588
- const code = fs.readFileSync(fullPath, 'utf-8');
589
- const tokens = new Lexer(code).getTokens();
590
- 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();
591
629
 
592
- const moduleEnv = new Environment(env);
593
- await this.evaluate(ast, moduleEnv);
630
+ const moduleEnv = new Environment(env);
631
+ await this.evaluate(ast, moduleEnv);
594
632
 
595
- lib = {};
596
- for (const key of Object.keys(moduleEnv.store)) {
597
- lib[key] = moduleEnv.store[key];
598
- }
633
+ lib = {};
634
+ for (const key of Object.keys(moduleEnv.store)) {
635
+ lib[key] = moduleEnv.store[key];
636
+ }
599
637
 
600
- 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
+ }
601
647
  }
602
648
 
603
649
  for (const imp of node.specifiers) {
604
650
  if (imp.type === 'DefaultImport') {
605
651
  env.define(imp.local, lib.default ?? lib);
606
- }
607
- if (imp.type === 'NamespaceImport') {
652
+ } else if (imp.type === 'NamespaceImport') {
608
653
  env.define(imp.local, lib);
609
- }
610
- if (imp.type === 'NamedImport') {
654
+ } else if (imp.type === 'NamedImport') {
611
655
  if (!(imp.imported in lib)) {
612
- 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
+ );
613
662
  }
614
663
  env.define(imp.local, lib[imp.imported]);
615
664
  }
@@ -617,7 +666,6 @@ async evalImport(node, env) {
617
666
 
618
667
  return null;
619
668
  }
620
-
621
669
  async evalBlock(node, env) {
622
670
  let result = null;
623
671
  for (const stmt of node.body) {
@@ -636,32 +684,55 @@ async evalBlock(node, env) {
636
684
  throw new RuntimeError(
637
685
  e.message || 'Error in block',
638
686
  stmt,
639
- this.source
687
+ this.source,
688
+ env // pass env for suggestions
640
689
  );
641
690
  }
642
691
  }
643
692
  return result;
644
693
  }
645
694
 
646
-
647
-
648
695
  async evalVarDeclaration(node, env) {
649
696
  if (!node.expr) {
650
- 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
+ );
651
703
  }
652
704
 
653
- const val = await this.evaluate(node.expr, env);
654
- 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
+ }
655
717
  }
656
718
 
657
-
658
719
  evalArrowFunction(node, env) {
659
720
  if (!node.body) {
660
- 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
+ );
661
727
  }
662
728
 
663
729
  if (!Array.isArray(node.params)) {
664
- 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
+ );
665
736
  }
666
737
 
667
738
  const evaluator = this;
@@ -669,7 +740,6 @@ evalArrowFunction(node, env) {
669
740
  return async function (...args) {
670
741
  const localEnv = new Environment(env);
671
742
 
672
- // Bind parameters safely
673
743
  node.params.forEach((p, i) => {
674
744
  const paramName = typeof p === 'string' ? p : p.name;
675
745
  localEnv.define(paramName, args[i]);
@@ -677,77 +747,127 @@ evalArrowFunction(node, env) {
677
747
 
678
748
  try {
679
749
  if (node.isBlock) {
680
- // Block body
681
750
  const result = await evaluator.evaluate(node.body, localEnv);
682
- return result === undefined ? null : result; // ensure null instead of undefined
751
+ return result === undefined ? null : result;
683
752
  } else {
684
- // Expression body
685
753
  const result = await evaluator.evaluate(node.body, localEnv);
686
- return result === undefined ? null : result; // ensure null instead of undefined
754
+ return result === undefined ? null : result;
687
755
  }
688
756
  } catch (err) {
689
757
  if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
690
- 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
+ );
691
765
  }
692
766
  };
693
767
  }
694
768
 
695
-
696
-
697
769
  async evalAssignment(node, env) {
698
770
  const rightVal = await this.evaluate(node.right, env);
699
771
  const left = node.left;
700
772
 
701
- 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
+ }
787
+
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
+ }
702
800
 
703
- if (left.type === 'MemberExpression') {
704
- const obj = await this.evaluate(left.object, env);
705
- if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
706
- obj[left.property] = rightVal; // dynamic creation of new properties allowed
707
- return rightVal;
708
- }
801
+ throw new RuntimeError(
802
+ 'Invalid assignment target',
803
+ node,
804
+ this.source,
805
+ env
806
+ );
709
807
 
710
- if (left.type === 'IndexExpression') {
711
- const obj = await this.evaluate(left.object, env);
712
- const idx = await this.evaluate(left.indexer, env);
713
- if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
714
- obj[idx] = rightVal; // dynamic creation allowed
715
- return rightVal;
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
+ );
716
816
  }
717
-
718
- throw new RuntimeError('Invalid assignment target', node, this.source);
719
817
  }
720
818
 
721
819
  async evalCompoundAssignment(node, env) {
722
820
  const left = node.left;
723
821
  let current;
724
822
 
725
- if (left.type === 'Identifier') current = env.get(left.name, left, this.source);
726
- else if (left.type === 'MemberExpression') current = await this.evalMember(left, env) ?? 0;
727
- else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env) ?? 0;
728
- else throw new RuntimeError('Invalid compound assignment target', node, this.source);
729
-
730
- const rhs = await this.evaluate(node.right, env);
731
- let computed;
732
-
733
- switch (node.operator) {
734
- case 'PLUSEQ': computed = current + rhs; break;
735
- case 'MINUSEQ': computed = current - rhs; break;
736
- case 'STAREQ': computed = current * rhs; break;
737
- case 'SLASHEQ': computed = current / rhs; break;
738
- case 'MODEQ': computed = current % rhs; break;
739
- default: throw new RuntimeError(`Unknown compound operator: ${node.operator}`, node, this.source);
740
- }
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
+ );
833
+
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
+ }
741
850
 
742
- if (left.type === 'Identifier') env.set(left.name, computed);
743
- else if (left.type === 'MemberExpression')
744
- await this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
745
- else
746
- await this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
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
+ }
747
858
 
748
- return computed;
749
- }
859
+ return computed;
750
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
+ }
751
871
 
752
872
  async evalSldeploy(node, env) {
753
873
  const val = await this.evaluate(node.expr, env);
@@ -756,111 +876,162 @@ async evalSldeploy(node, env) {
756
876
  }
757
877
 
758
878
 
759
-
760
879
  async evalAsk(node, env) {
761
- const prompt = await this.evaluate(node.prompt, env);
762
-
763
- if (typeof prompt !== 'string') {
764
- throw new RuntimeError('ask() prompt must be a string', node, this.source);
765
- }
880
+ try {
881
+ const prompt = await this.evaluate(node.prompt, env);
766
882
 
767
- const input = readlineSync.question(prompt + ' ');
768
- return input;
769
- }
883
+ if (typeof prompt !== 'string') {
884
+ throw new RuntimeError('ask() prompt must be a string', node, this.source, env);
885
+ }
770
886
 
887
+ const input = readlineSync.question(prompt + ' ');
888
+ return input;
771
889
 
772
- async evalDefine(node, env) {
773
- if (!node.id || typeof node.id !== 'string') {
774
- 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
+ );
775
898
  }
776
-
777
- const val = node.expr ? await this.evaluate(node.expr, env) : null;
778
- return env.define(node.id, val);
779
-
780
899
  }
781
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
+ }
782
906
 
783
- async evalBinary(node, env) {
784
- const l = await this.evaluate(node.left, env);
785
- const r = await this.evaluate(node.right, env);
786
-
787
- if (node.operator === 'SLASH' && r === 0) {
788
- throw new RuntimeError('Division by zero', node, this.source);
789
- }
790
-
791
- switch (node.operator) {
792
- case 'PLUS': {
793
- if (Array.isArray(l) && Array.isArray(r)) {
794
- return l.concat(r);
795
- }
907
+ const val = node.expr ? await this.evaluate(node.expr, env) : null;
908
+ return env.define(node.id, val);
796
909
 
797
- if (typeof l === 'string' || typeof r === 'string') {
798
- 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
+ );
799
918
  }
919
+ }
800
920
 
801
- if (typeof l === 'number' && typeof r === 'number') {
802
- return l + r;
803
- }
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);
804
925
 
805
- if (typeof l === 'object' && typeof r === 'object') {
806
- return { ...l, ...r };
807
- }
926
+ if (node.operator === 'SLASH' && r === 0) {
927
+ throw new RuntimeError('Division by zero', node, this.source, env);
928
+ }
808
929
 
809
- throw new RuntimeError(
810
- `Unsupported operands for +: ${typeof l} and ${typeof r}`,
811
- node,
812
- this.source
813
- );
814
- }
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 };
815
936
 
816
- case 'MINUS': return l - r;
817
- case 'STAR': return l * r;
818
- case 'SLASH': return l / r;
819
- case 'MOD': return l % r;
820
- case 'EQEQ': return l === r;
821
- case 'NOTEQ': return l !== r;
822
- case 'LT': return l < r;
823
- case 'LTE': return l <= r;
824
- case 'GT': return l > r;
825
- case 'GTE': return l >= r;
826
- 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
+ }
827
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
+ );
828
971
  }
829
972
  }
830
-
831
973
  async evalLogical(node, env) {
832
- const l = await this.evaluate(node.left, env);
833
-
834
- switch(node.operator) {
835
- case 'AND': return l && await this.evaluate(node.right, env);
836
- case 'OR': return l || await this.evaluate(node.right, env);
837
- case '??': {
838
- const r = await this.evaluate(node.right, env);
839
- 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
+ );
840
991
  }
841
- default:
842
- 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
+ );
843
1001
  }
844
1002
  }
845
1003
 
846
-
847
1004
  async evalUnary(node, env) {
848
- const val = await this.evaluate(node.argument, env);
849
- switch (node.operator) {
850
- case 'NOT': return !val;
851
- case 'MINUS': return -val;
852
- case 'PLUS': return +val;
853
- default: throw new RuntimeError(`Unknown unary operator: ${node.operator}`, node, this.source);
1005
+ try {
1006
+ const val = await this.evaluate(node.argument, env);
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
+ }
854
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
+ );
855
1029
  }
856
1030
  }
857
1031
 
858
1032
  async evalIf(node, env) {
859
- const test = await this.evaluate(node.test, env);
860
-
861
- if (typeof test !== 'boolean') {
862
- throw new RuntimeError('If condition must evaluate to a boolean', node.test, this.source);
863
- }
1033
+ let test = await this.evaluate(node.test, env);
1034
+ test = !!test; // coerce to boolean
864
1035
 
865
1036
  if (test) {
866
1037
  return await this.evaluate(node.consequent, env);
@@ -873,222 +1044,344 @@ async evalIf(node, env) {
873
1044
  return null;
874
1045
  }
875
1046
 
876
-
877
1047
  async evalWhile(node, env) {
878
- while (true) {
879
- const test = await this.evaluate(node.test, env);
1048
+ try {
1049
+ while (true) {
1050
+ let test = await this.evaluate(node.test, env);
1051
+ test = !!test;
880
1052
 
881
- if (typeof test !== 'boolean') {
882
- throw new RuntimeError('While condition must evaluate to a boolean', node.test, this.source);
1053
+ if (!test) break;
1054
+
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
+ }
883
1062
  }
884
1063
 
885
- if (!test) break;
1064
+ return null;
886
1065
 
887
- try {
888
- await this.evaluate(node.body, env);
889
- } catch (e) {
890
- if (e instanceof BreakSignal) break;
891
- if (e instanceof ContinueSignal) continue;
892
- throw e;
893
- }
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
+ );
894
1074
  }
895
- return null;
896
1075
  }
897
1076
  async evalFor(node, env) {
898
- if (node.type === 'ForInStatement') {
899
- 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);
900
1081
 
901
- if (iterable == null || typeof iterable !== 'object') {
902
- throw new RuntimeError('Cannot iterate over non-iterable', node, this.source);
903
- }
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
+ }
904
1090
 
905
- const loopVar = node.variable; // string name of the loop variable
906
- const createLoopEnv = () => node.letKeyword ? new Environment(env) : env;
907
- if (Array.isArray(iterable)) {
908
- for (const value of iterable) {
909
- const loopEnv = createLoopEnv();
910
- loopEnv.define(loopVar, value);
911
-
912
- try {
913
- await this.evaluate(node.body, loopEnv);
914
- } catch (e) {
915
- if (e instanceof BreakSignal) break;
916
- if (e instanceof ContinueSignal) continue;
917
- 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
+ }
918
1106
  }
919
- }
920
- }
921
- else {
922
- for (const key of Object.keys(iterable)) {
923
- const loopEnv = createLoopEnv();
924
- loopEnv.define(loopVar, key);
925
-
926
- try {
927
- await this.evaluate(node.body, loopEnv);
928
- } catch (e) {
929
- if (e instanceof BreakSignal) break;
930
- if (e instanceof ContinueSignal) continue;
931
- 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
+ }
932
1119
  }
933
1120
  }
934
- }
935
1121
 
936
- return null;
937
- }
1122
+ return null;
1123
+ }
938
1124
 
939
- const local = new Environment(env);
1125
+ // Standard for loop
1126
+ const local = new Environment(env);
940
1127
 
941
- if (node.init) await this.evaluate(node.init, local);
1128
+ if (node.init) await this.evaluate(node.init, local);
942
1129
 
943
- while (!node.test || await this.evaluate(node.test, local)) {
944
- try {
945
- await this.evaluate(node.body, local);
946
- } catch (e) {
947
- if (e instanceof BreakSignal) break;
948
- if (e instanceof ContinueSignal) {
949
- if (node.update) await this.evaluate(node.update, local);
950
- 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;
951
1140
  }
952
- throw e;
1141
+
1142
+ if (node.update) await this.evaluate(node.update, local);
953
1143
  }
954
1144
 
955
- if (node.update) await this.evaluate(node.update, local);
956
- }
1145
+ return null;
957
1146
 
958
- 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
+ }
959
1156
  }
960
1157
 
961
1158
  evalFunctionDeclaration(node, env) {
962
- if (!node.name || typeof node.name !== 'string') {
963
- throw new RuntimeError('Function declaration requires a valid name', node, this.source);
964
- }
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
+ }
965
1163
 
966
- if (!Array.isArray(node.params)) {
967
- throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source);
968
- }
1164
+ if (!Array.isArray(node.params)) {
1165
+ throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source, env);
1166
+ }
969
1167
 
970
- if (!node.body) {
971
- throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source);
972
- }
1168
+ if (!node.body) {
1169
+ throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source, env);
1170
+ }
973
1171
 
974
- const fn = {
975
- params: node.params,
976
- body: node.body,
977
- env,
978
- async: node.async || false
979
- };
1172
+ const fn = {
1173
+ params: node.params,
1174
+ body: node.body,
1175
+ env,
1176
+ async: node.async || false
1177
+ };
980
1178
 
981
- env.define(node.name, fn);
982
- return null;
983
- }
1179
+ env.define(node.name, fn);
1180
+ return null;
984
1181
 
985
- async evalCall(node, env) {
986
- const calleeEvaluated = await this.evaluate(node.callee, env);
987
- if (typeof calleeEvaluated === 'function') {
988
- const args = [];
989
- for (const a of node.arguments) args.push(await this.evaluate(a, env));
990
- return await calleeEvaluated(...args);
991
- }
992
- if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
993
- 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
+ );
994
1190
  }
1191
+ }
1192
+ async evalCall(node, env) {
1193
+ try {
1194
+ const calleeEvaluated = await this.evaluate(node.callee, env);
995
1195
 
996
- const fn = calleeEvaluated;
997
- 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
+ }
998
1212
 
999
- for (let i = 0; i < fn.params.length; i++) {
1000
- const argVal = node.arguments[i]
1001
- ? await this.evaluate(node.arguments[i], env)
1002
- : null;
1213
+ const fn = calleeEvaluated;
1214
+ const callEnv = new Environment(fn.env);
1003
1215
 
1004
- const param = fn.params[i];
1005
- 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;
1006
1220
 
1007
- callEnv.define(paramName, argVal);
1008
- }
1221
+ const param = fn.params[i];
1222
+ const paramName = typeof param === 'string' ? param : param.name;
1223
+ callEnv.define(paramName, argVal);
1224
+ }
1009
1225
 
1010
- try {
1011
- const result = await this.evaluate(fn.body, callEnv);
1012
- return result;
1013
- } catch (e) {
1014
- if (e instanceof ReturnValue) return e.value;
1015
- 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
+ );
1016
1242
  }
1017
1243
  }
1018
1244
 
1019
1245
  async evalIndex(node, env) {
1020
- const obj = await this.evaluate(node.object, env);
1021
- const idx = await this.evaluate(node.indexer, env);
1022
-
1023
- if (obj == null) throw new RuntimeError('Cannot index null or undefined', node, this.source);
1024
- if (Array.isArray(obj)) {
1025
- if (idx < 0 || idx >= obj.length) return undefined;
1026
- return obj[idx];
1027
- }
1028
- if (typeof obj === 'object') {
1029
- return obj[idx]; // undefined if missing
1030
- }
1246
+ try {
1247
+ const obj = await this.evaluate(node.object, env);
1248
+ const idx = await this.evaluate(node.indexer, env);
1031
1249
 
1032
- return undefined;
1033
- }
1034
- async evalObject(node, env) {
1035
- const out = {};
1036
- for (const p of node.props) {
1037
- if (!p.key) {
1038
- 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
+ );
1039
1257
  }
1040
1258
 
1041
- const key = await this.evaluate(p.key, env);
1042
- let value = null;
1043
-
1044
- if (p.value) {
1045
- value = await this.evaluate(p.value, env);
1046
- 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];
1047
1262
  }
1048
1263
 
1049
- 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
+ );
1050
1275
  }
1051
- return out;
1052
1276
  }
1053
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
+ }
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
+ }
1054
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
+ }
1055
1312
 
1056
1313
 
1057
1314
  async evalMember(node, env) {
1058
1315
  const obj = await this.evaluate(node.object, env);
1059
1316
 
1060
- if (obj == null) throw new RuntimeError('Member access of null or undefined', node, this.source);
1317
+ if (obj == null) {
1318
+ throw new RuntimeError('Member access of null or undefined', node, this.source);
1319
+ }
1061
1320
 
1062
- return obj[node.property];
1063
- }
1321
+ const prop = obj[node.property];
1064
1322
 
1323
+ if (typeof prop === 'function') {
1324
+ return prop.bind(obj);
1325
+ }
1326
+
1327
+ return prop;
1328
+ }
1065
1329
 
1066
1330
  async evalUpdate(node, env) {
1067
1331
  const arg = node.argument;
1068
- const getCurrent = async () => {
1069
- if (arg.type === 'Identifier') return env.get(arg.name, arg, this.source);
1070
- if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
1071
- if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
1072
- throw new RuntimeError('Invalid update target', node, this.source);
1073
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
+ }
1074
1348
  };
1349
+
1075
1350
  const setValue = async (v) => {
1076
- if (arg.type === 'Identifier') env.set(arg.name, v);
1077
- else if (arg.type === 'MemberExpression') {
1078
- const obj = await this.evaluate(arg.object, env);
1079
- obj[arg.property] = v;
1080
- } else if (arg.type === 'IndexExpression') {
1081
- const obj = await this.evaluate(arg.object, env);
1082
- const idx = await this.evaluate(arg.indexer, env);
1083
- 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
+ );
1084
1372
  }
1085
1373
  };
1086
1374
 
1087
1375
  const current = await getCurrent();
1088
- const newVal = (node.operator === 'PLUSPLUS') ? current + 1 : current - 1;
1376
+ const newVal = node.operator === 'PLUSPLUS' ? current + 1 : current - 1;
1089
1377
 
1090
- if (node.prefix) { await setValue(newVal); return newVal; }
1091
- 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
+ }
1092
1385
  }
1093
1386
 
1094
1387
  }