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/dist/index.js +1517 -517
- package/package.json +1 -1
- package/src/evaluator.js +625 -350
- package/src/parser.js +886 -161
- package/src/starlight.js +1 -1
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
typeof column === 'number' && column > 0
|
|
24
|
+
? column - 1
|
|
25
|
+
: 0;
|
|
26
|
+
output += ` ${' '.repeat(caretPos)}^\n`;
|
|
27
|
+
}
|
|
27
28
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
617
|
+
throw new RuntimeError(
|
|
618
|
+
`Import not found: ${spec}`,
|
|
619
|
+
node,
|
|
620
|
+
this.source,
|
|
621
|
+
env
|
|
622
|
+
);
|
|
600
623
|
}
|
|
601
624
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
607
|
-
|
|
630
|
+
const moduleEnv = new Environment(env);
|
|
631
|
+
await this.evaluate(ast, moduleEnv);
|
|
608
632
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
633
|
+
lib = {};
|
|
634
|
+
for (const key of Object.keys(moduleEnv.store)) {
|
|
635
|
+
lib[key] = moduleEnv.store[key];
|
|
636
|
+
}
|
|
613
637
|
|
|
614
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
668
|
-
|
|
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(
|
|
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(
|
|
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;
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
801
|
+
throw new RuntimeError(
|
|
802
|
+
'Invalid assignment target',
|
|
803
|
+
node,
|
|
804
|
+
this.source,
|
|
805
|
+
env
|
|
806
|
+
);
|
|
731
807
|
|
|
732
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
782
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
throw new RuntimeError(
|
|
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
|
-
|
|
798
|
-
|
|
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
|
-
|
|
812
|
-
|
|
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
|
-
|
|
816
|
-
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
926
|
+
if (node.operator === 'SLASH' && r === 0) {
|
|
927
|
+
throw new RuntimeError('Division by zero', node, this.source, env);
|
|
928
|
+
}
|
|
822
929
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
|
|
856
|
-
|
|
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
|
-
|
|
863
|
-
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1048
|
+
try {
|
|
1049
|
+
while (true) {
|
|
1050
|
+
let test = await this.evaluate(node.test, env);
|
|
1051
|
+
test = !!test;
|
|
892
1052
|
|
|
893
|
-
|
|
1053
|
+
if (!test) break;
|
|
894
1054
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
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
|
-
|
|
909
|
-
|
|
1077
|
+
try {
|
|
1078
|
+
// ForInStatement
|
|
1079
|
+
if (node.type === 'ForInStatement') {
|
|
1080
|
+
const iterable = await this.evaluate(node.iterable, env);
|
|
910
1081
|
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
947
|
-
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
948
1124
|
|
|
949
|
-
|
|
1125
|
+
// Standard for loop
|
|
1126
|
+
const local = new Environment(env);
|
|
950
1127
|
|
|
951
|
-
|
|
1128
|
+
if (node.init) await this.evaluate(node.init, local);
|
|
952
1129
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
1141
|
+
|
|
1142
|
+
if (node.update) await this.evaluate(node.update, local);
|
|
963
1143
|
}
|
|
964
1144
|
|
|
965
|
-
|
|
966
|
-
}
|
|
1145
|
+
return null;
|
|
967
1146
|
|
|
968
|
-
|
|
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
|
-
|
|
973
|
-
|
|
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
|
-
|
|
977
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1168
|
+
if (!node.body) {
|
|
1169
|
+
throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source, env);
|
|
1170
|
+
}
|
|
983
1171
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1172
|
+
const fn = {
|
|
1173
|
+
params: node.params,
|
|
1174
|
+
body: node.body,
|
|
1175
|
+
env,
|
|
1176
|
+
async: node.async || false
|
|
1177
|
+
};
|
|
990
1178
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}
|
|
1179
|
+
env.define(node.name, fn);
|
|
1180
|
+
return null;
|
|
994
1181
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
? await this.evaluate(node.arguments[i], env)
|
|
1012
|
-
: null;
|
|
1213
|
+
const fn = calleeEvaluated;
|
|
1214
|
+
const callEnv = new Environment(fn.env);
|
|
1013
1215
|
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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 =
|
|
1376
|
+
const newVal = node.operator === 'PLUSPLUS' ? current + 1 : current - 1;
|
|
1107
1377
|
|
|
1108
|
-
if (node.prefix) {
|
|
1109
|
-
|
|
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
|
}
|