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/dist/index.js +1574 -520
- package/package.json +1 -1
- package/src/evaluator.js +648 -355
- package/src/lexer.js +3 -1
- package/src/parser.js +917 -158
- 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
|
-
|
|
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
|
-
|
|
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 (
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
617
|
+
throw new RuntimeError(
|
|
618
|
+
`Import not found: ${spec}`,
|
|
619
|
+
node,
|
|
620
|
+
this.source,
|
|
621
|
+
env
|
|
622
|
+
);
|
|
586
623
|
}
|
|
587
624
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
593
|
-
|
|
630
|
+
const moduleEnv = new Environment(env);
|
|
631
|
+
await this.evaluate(ast, moduleEnv);
|
|
594
632
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
633
|
+
lib = {};
|
|
634
|
+
for (const key of Object.keys(moduleEnv.store)) {
|
|
635
|
+
lib[key] = moduleEnv.store[key];
|
|
636
|
+
}
|
|
599
637
|
|
|
600
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
654
|
-
|
|
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(
|
|
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(
|
|
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;
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
801
|
+
throw new RuntimeError(
|
|
802
|
+
'Invalid assignment target',
|
|
803
|
+
node,
|
|
804
|
+
this.source,
|
|
805
|
+
env
|
|
806
|
+
);
|
|
709
807
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
768
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
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
|
+
);
|
|
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
|
-
|
|
784
|
-
|
|
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
|
-
|
|
798
|
-
|
|
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
|
-
|
|
802
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
926
|
+
if (node.operator === 'SLASH' && r === 0) {
|
|
927
|
+
throw new RuntimeError('Division by zero', node, this.source, env);
|
|
928
|
+
}
|
|
808
929
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
842
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
|
|
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
|
-
|
|
879
|
-
|
|
1048
|
+
try {
|
|
1049
|
+
while (true) {
|
|
1050
|
+
let test = await this.evaluate(node.test, env);
|
|
1051
|
+
test = !!test;
|
|
880
1052
|
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
1064
|
+
return null;
|
|
886
1065
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
899
|
-
|
|
1077
|
+
try {
|
|
1078
|
+
// ForInStatement
|
|
1079
|
+
if (node.type === 'ForInStatement') {
|
|
1080
|
+
const iterable = await this.evaluate(node.iterable, env);
|
|
900
1081
|
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
937
|
-
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
938
1124
|
|
|
939
|
-
|
|
1125
|
+
// Standard for loop
|
|
1126
|
+
const local = new Environment(env);
|
|
940
1127
|
|
|
941
|
-
|
|
1128
|
+
if (node.init) await this.evaluate(node.init, local);
|
|
942
1129
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
1141
|
+
|
|
1142
|
+
if (node.update) await this.evaluate(node.update, local);
|
|
953
1143
|
}
|
|
954
1144
|
|
|
955
|
-
|
|
956
|
-
}
|
|
1145
|
+
return null;
|
|
957
1146
|
|
|
958
|
-
|
|
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
|
-
|
|
963
|
-
|
|
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
|
-
|
|
967
|
-
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1168
|
+
if (!node.body) {
|
|
1169
|
+
throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source, env);
|
|
1170
|
+
}
|
|
973
1171
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1172
|
+
const fn = {
|
|
1173
|
+
params: node.params,
|
|
1174
|
+
body: node.body,
|
|
1175
|
+
env,
|
|
1176
|
+
async: node.async || false
|
|
1177
|
+
};
|
|
980
1178
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
}
|
|
1179
|
+
env.define(node.name, fn);
|
|
1180
|
+
return null;
|
|
984
1181
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
? await this.evaluate(node.arguments[i], env)
|
|
1002
|
-
: null;
|
|
1213
|
+
const fn = calleeEvaluated;
|
|
1214
|
+
const callEnv = new Environment(fn.env);
|
|
1003
1215
|
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
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)
|
|
1317
|
+
if (obj == null) {
|
|
1318
|
+
throw new RuntimeError('Member access of null or undefined', node, this.source);
|
|
1319
|
+
}
|
|
1061
1320
|
|
|
1062
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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 =
|
|
1376
|
+
const newVal = node.operator === 'PLUSPLUS' ? current + 1 : current - 1;
|
|
1089
1377
|
|
|
1090
|
-
if (node.prefix) {
|
|
1091
|
-
|
|
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
|
}
|