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