starlight-cli 1.1.8 → 1.1.10
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 +1 -1
- package/dist/index.js +240 -95
- package/package.json +1 -1
- package/src/evaluator.js +177 -71
- package/src/parser.js +62 -23
- package/src/starlight.js +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Starlight is a lightweight, developer-oriented programming language designed for **server-side scripting, automation, and general-purpose programming**. It combines a clean, readable syntax inspired by JavaScript and Python with powerful runtime features such as async/await, modules, and interactive I/O.
|
|
5
5
|
|
|
6
6
|
**Official Reference:**
|
|
7
|
-
https://starlight-learn
|
|
7
|
+
https://dominexmacedon.github.io/starlight-programming-language/learn.html
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
package/dist/index.js
CHANGED
|
@@ -10230,7 +10230,13 @@ class RuntimeError extends Error {
|
|
|
10230
10230
|
const lines = source.split('\n');
|
|
10231
10231
|
const srcLine = lines[node.line - 1] || '';
|
|
10232
10232
|
output += ` ${srcLine}\n`;
|
|
10233
|
-
|
|
10233
|
+
const caretPos =
|
|
10234
|
+
typeof column === 'number' && column > 0
|
|
10235
|
+
? column - 1
|
|
10236
|
+
: 0;
|
|
10237
|
+
|
|
10238
|
+
output += ` ${' '.repeat(caretPos)}^\n`;
|
|
10239
|
+
;
|
|
10234
10240
|
}
|
|
10235
10241
|
|
|
10236
10242
|
super(output);
|
|
@@ -10292,6 +10298,34 @@ class Evaluator {
|
|
|
10292
10298
|
this.global = new Environment();
|
|
10293
10299
|
this.setupBuiltins();
|
|
10294
10300
|
}
|
|
10301
|
+
async callFunction(fn, args, env, node = null) {
|
|
10302
|
+
if (typeof fn === 'function') {
|
|
10303
|
+
const val = await fn(...args);
|
|
10304
|
+
return val === undefined ? null : val; // <<< enforce null
|
|
10305
|
+
}
|
|
10306
|
+
|
|
10307
|
+
if (fn && typeof fn === 'object' && fn.body && fn.params) {
|
|
10308
|
+
const callEnv = new Environment(fn.env);
|
|
10309
|
+
|
|
10310
|
+
for (let i = 0; i < fn.params.length; i++) {
|
|
10311
|
+
const param = fn.params[i];
|
|
10312
|
+
const name = typeof param === 'string' ? param : param.name;
|
|
10313
|
+
callEnv.define(name, args[i]);
|
|
10314
|
+
}
|
|
10315
|
+
|
|
10316
|
+
try {
|
|
10317
|
+
const val = await this.evaluate(fn.body, callEnv);
|
|
10318
|
+
return val === undefined ? null : val; // <<< enforce null
|
|
10319
|
+
} catch (e) {
|
|
10320
|
+
if (e instanceof ReturnValue) return e.value === undefined ? null : e.value; // <<< enforce null
|
|
10321
|
+
throw e;
|
|
10322
|
+
}
|
|
10323
|
+
}
|
|
10324
|
+
|
|
10325
|
+
throw new RuntimeError('Value is not callable', node, this.source);
|
|
10326
|
+
}
|
|
10327
|
+
|
|
10328
|
+
|
|
10295
10329
|
suggest(name, env) {
|
|
10296
10330
|
const names = new Set();
|
|
10297
10331
|
let current = env;
|
|
@@ -10365,6 +10399,7 @@ formatValue(value, seen = new Set()) {
|
|
|
10365
10399
|
|
|
10366
10400
|
|
|
10367
10401
|
setupBuiltins() {
|
|
10402
|
+
const evaluator = this;
|
|
10368
10403
|
this.global.define('len', arg => {
|
|
10369
10404
|
if (Array.isArray(arg) || typeof arg === 'string') return arg.length;
|
|
10370
10405
|
if (arg && typeof arg === 'object') return Object.keys(arg).length;
|
|
@@ -10376,63 +10411,76 @@ formatValue(value, seen = new Set()) {
|
|
|
10376
10411
|
if (Array.isArray(arg)) return 'array';
|
|
10377
10412
|
return typeof arg;
|
|
10378
10413
|
});
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
if (typeof fn !== 'function') {
|
|
10384
|
-
throw new RuntimeError('map() expects a function', null, this.source);
|
|
10385
|
-
}
|
|
10414
|
+
this.global.define('map', async (array, fn) => {
|
|
10415
|
+
if (!Array.isArray(array)) {
|
|
10416
|
+
throw new RuntimeError('map() expects an array', null, evaluator.source);
|
|
10417
|
+
}
|
|
10386
10418
|
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
}
|
|
10419
|
+
const result = [];
|
|
10420
|
+
for (let i = 0; i < array.length; i++) {
|
|
10421
|
+
result.push(
|
|
10422
|
+
await evaluator.callFunction(
|
|
10423
|
+
fn,
|
|
10424
|
+
[array[i], i, array],
|
|
10425
|
+
evaluator.global
|
|
10426
|
+
)
|
|
10427
|
+
);
|
|
10428
|
+
}
|
|
10429
|
+
return result;
|
|
10430
|
+
});
|
|
10400
10431
|
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
result.push(array[i]);
|
|
10432
|
+
this.global.define('filter', async (array, fn) => {
|
|
10433
|
+
if (!Array.isArray(array)) {
|
|
10434
|
+
throw new RuntimeError('filter() expects an array', null, evaluator.source);
|
|
10405
10435
|
}
|
|
10406
|
-
}
|
|
10407
|
-
return result;
|
|
10408
|
-
});
|
|
10409
|
-
this.global.define('reduce', async (array, fn, initial) => {
|
|
10410
|
-
if (!Array.isArray(array)) {
|
|
10411
|
-
throw new RuntimeError('reduce() expects an array', null, this.source);
|
|
10412
|
-
}
|
|
10413
|
-
if (typeof fn !== 'function') {
|
|
10414
|
-
throw new RuntimeError('reduce() expects a function', null, this.source);
|
|
10415
|
-
}
|
|
10416
10436
|
|
|
10417
|
-
|
|
10418
|
-
|
|
10437
|
+
const result = [];
|
|
10438
|
+
for (let i = 0; i < array.length; i++) {
|
|
10439
|
+
if (
|
|
10440
|
+
await evaluator.callFunction(
|
|
10441
|
+
fn,
|
|
10442
|
+
[array[i], i, array],
|
|
10443
|
+
evaluator.global
|
|
10444
|
+
)
|
|
10445
|
+
) {
|
|
10446
|
+
result.push(array[i]);
|
|
10447
|
+
}
|
|
10448
|
+
}
|
|
10449
|
+
return result;
|
|
10450
|
+
});
|
|
10419
10451
|
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
if (array.length === 0) {
|
|
10424
|
-
throw new RuntimeError('reduce() of empty array with no initial value', null, this.source);
|
|
10452
|
+
this.global.define('reduce', async (array, fn, initial) => {
|
|
10453
|
+
if (!Array.isArray(array)) {
|
|
10454
|
+
throw new RuntimeError('reduce() expects an array', null, evaluator.source);
|
|
10425
10455
|
}
|
|
10426
|
-
acc = array[0];
|
|
10427
|
-
startIndex = 1;
|
|
10428
|
-
}
|
|
10429
10456
|
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
}
|
|
10457
|
+
let acc;
|
|
10458
|
+
let i = 0;
|
|
10433
10459
|
|
|
10434
|
-
|
|
10435
|
-
|
|
10460
|
+
if (initial !== undefined) {
|
|
10461
|
+
acc = initial;
|
|
10462
|
+
} else {
|
|
10463
|
+
if (array.length === 0) {
|
|
10464
|
+
throw new RuntimeError(
|
|
10465
|
+
'reduce() of empty array with no initial value',
|
|
10466
|
+
null,
|
|
10467
|
+
evaluator.source
|
|
10468
|
+
);
|
|
10469
|
+
}
|
|
10470
|
+
acc = array[0];
|
|
10471
|
+
i = 1;
|
|
10472
|
+
}
|
|
10473
|
+
|
|
10474
|
+
for (; i < array.length; i++) {
|
|
10475
|
+
acc = await evaluator.callFunction(
|
|
10476
|
+
fn,
|
|
10477
|
+
[acc, array[i], i, array],
|
|
10478
|
+
evaluator.global
|
|
10479
|
+
);
|
|
10480
|
+
}
|
|
10481
|
+
|
|
10482
|
+
return acc;
|
|
10483
|
+
});
|
|
10436
10484
|
|
|
10437
10485
|
this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
|
|
10438
10486
|
this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
|
|
@@ -10477,12 +10525,26 @@ this.global.define('range', (...args) => {
|
|
|
10477
10525
|
return readlineSync.question(prompt + ' ');
|
|
10478
10526
|
});
|
|
10479
10527
|
this.global.define('num', arg => {
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10528
|
+
if (arg === null || arg === undefined) return 0;
|
|
10529
|
+
|
|
10530
|
+
const t = typeof arg;
|
|
10531
|
+
|
|
10532
|
+
if (t === 'number') return arg; // already a number
|
|
10533
|
+
if (t === 'boolean') return arg ? 1 : 0;
|
|
10534
|
+
if (t === 'string') {
|
|
10535
|
+
const n = Number(arg);
|
|
10536
|
+
if (!Number.isNaN(n)) return n; // numeric string
|
|
10537
|
+
return 0; // non-numeric string becomes 0
|
|
10483
10538
|
}
|
|
10484
|
-
return
|
|
10539
|
+
if (Array.isArray(arg)) return arg.length; // array → length
|
|
10540
|
+
if (t === 'object') return Object.keys(arg).length; // object → number of keys
|
|
10541
|
+
|
|
10542
|
+
// fallback for anything else
|
|
10543
|
+
const n = Number(arg);
|
|
10544
|
+
if (!Number.isNaN(n)) return n;
|
|
10545
|
+
return 0;
|
|
10485
10546
|
});
|
|
10547
|
+
|
|
10486
10548
|
this.global.define('fetch', async (url, options = {}) => {
|
|
10487
10549
|
const res = await fetch(url, options);
|
|
10488
10550
|
return {
|
|
@@ -10572,21 +10634,30 @@ case 'RaceClause':
|
|
|
10572
10634
|
const callee = await this.evaluate(node.callee, env);
|
|
10573
10635
|
|
|
10574
10636
|
if (typeof callee === 'object' && callee.body) {
|
|
10575
|
-
const evaluator = this;
|
|
10576
|
-
|
|
10637
|
+
const evaluator = this;
|
|
10638
|
+
|
|
10639
|
+
const Constructor = function (...args) {
|
|
10577
10640
|
const newEnv = new Environment(callee.env);
|
|
10578
10641
|
newEnv.define('this', this);
|
|
10642
|
+
|
|
10579
10643
|
for (let i = 0; i < callee.params.length; i++) {
|
|
10580
|
-
|
|
10644
|
+
const param = callee.params[i];
|
|
10645
|
+
const paramName = typeof param === 'string' ? param : param.name;
|
|
10646
|
+
newEnv.define(paramName, args[i]);
|
|
10581
10647
|
}
|
|
10582
|
-
|
|
10648
|
+
|
|
10649
|
+
return (async () => {
|
|
10650
|
+
await evaluator.evaluate(callee.body, newEnv);
|
|
10651
|
+
return this;
|
|
10652
|
+
})();
|
|
10583
10653
|
};
|
|
10584
10654
|
|
|
10585
10655
|
const args = [];
|
|
10586
10656
|
for (const a of node.arguments) args.push(await this.evaluate(a, env));
|
|
10587
|
-
return new Constructor(...args);
|
|
10657
|
+
return await new Constructor(...args);
|
|
10588
10658
|
}
|
|
10589
10659
|
|
|
10660
|
+
// native JS constructor fallback
|
|
10590
10661
|
if (typeof callee !== 'function') {
|
|
10591
10662
|
throw new RuntimeError('NewExpression callee is not a function', node, this.source);
|
|
10592
10663
|
}
|
|
@@ -10808,27 +10879,31 @@ evalArrowFunction(node, env) {
|
|
|
10808
10879
|
return async function (...args) {
|
|
10809
10880
|
const localEnv = new Environment(env);
|
|
10810
10881
|
|
|
10882
|
+
// Bind parameters safely
|
|
10811
10883
|
node.params.forEach((p, i) => {
|
|
10812
|
-
|
|
10884
|
+
const paramName = typeof p === 'string' ? p : p.name;
|
|
10885
|
+
localEnv.define(paramName, args[i]);
|
|
10813
10886
|
});
|
|
10814
10887
|
|
|
10815
10888
|
try {
|
|
10816
10889
|
if (node.isBlock) {
|
|
10890
|
+
// Block body
|
|
10817
10891
|
const result = await evaluator.evaluate(node.body, localEnv);
|
|
10818
|
-
return result
|
|
10892
|
+
return result === undefined ? null : result; // ensure null instead of undefined
|
|
10819
10893
|
} else {
|
|
10820
|
-
|
|
10894
|
+
// Expression body
|
|
10895
|
+
const result = await evaluator.evaluate(node.body, localEnv);
|
|
10896
|
+
return result === undefined ? null : result; // ensure null instead of undefined
|
|
10821
10897
|
}
|
|
10822
10898
|
} catch (err) {
|
|
10823
|
-
if (err instanceof ReturnValue)
|
|
10824
|
-
return err.value;
|
|
10825
|
-
}
|
|
10899
|
+
if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
|
|
10826
10900
|
throw err;
|
|
10827
10901
|
}
|
|
10828
10902
|
};
|
|
10829
10903
|
}
|
|
10830
10904
|
|
|
10831
10905
|
|
|
10906
|
+
|
|
10832
10907
|
async evalAssignment(node, env) {
|
|
10833
10908
|
const rightVal = await this.evaluate(node.right, env);
|
|
10834
10909
|
const left = node.left;
|
|
@@ -10924,7 +10999,30 @@ async evalBinary(node, env) {
|
|
|
10924
10999
|
}
|
|
10925
11000
|
|
|
10926
11001
|
switch (node.operator) {
|
|
10927
|
-
case 'PLUS':
|
|
11002
|
+
case 'PLUS': {
|
|
11003
|
+
if (Array.isArray(l) && Array.isArray(r)) {
|
|
11004
|
+
return l.concat(r);
|
|
11005
|
+
}
|
|
11006
|
+
|
|
11007
|
+
if (typeof l === 'string' || typeof r === 'string') {
|
|
11008
|
+
return String(l) + String(r);
|
|
11009
|
+
}
|
|
11010
|
+
|
|
11011
|
+
if (typeof l === 'number' && typeof r === 'number') {
|
|
11012
|
+
return l + r;
|
|
11013
|
+
}
|
|
11014
|
+
|
|
11015
|
+
if (typeof l === 'object' && typeof r === 'object') {
|
|
11016
|
+
return { ...l, ...r };
|
|
11017
|
+
}
|
|
11018
|
+
|
|
11019
|
+
throw new RuntimeError(
|
|
11020
|
+
`Unsupported operands for +: ${typeof l} and ${typeof r}`,
|
|
11021
|
+
node,
|
|
11022
|
+
this.source
|
|
11023
|
+
);
|
|
11024
|
+
}
|
|
11025
|
+
|
|
10928
11026
|
case 'MINUS': return l - r;
|
|
10929
11027
|
case 'STAR': return l * r;
|
|
10930
11028
|
case 'SLASH': return l / r;
|
|
@@ -11146,18 +11244,26 @@ async evalIndex(node, env) {
|
|
|
11146
11244
|
async evalObject(node, env) {
|
|
11147
11245
|
const out = {};
|
|
11148
11246
|
for (const p of node.props) {
|
|
11149
|
-
if (!p.key
|
|
11150
|
-
throw new RuntimeError('
|
|
11247
|
+
if (!p.key) {
|
|
11248
|
+
throw new RuntimeError('Object property must have a key', node, this.source);
|
|
11151
11249
|
}
|
|
11250
|
+
|
|
11152
11251
|
const key = await this.evaluate(p.key, env);
|
|
11153
|
-
|
|
11154
|
-
|
|
11252
|
+
let value = null;
|
|
11253
|
+
|
|
11254
|
+
if (p.value) {
|
|
11255
|
+
value = await this.evaluate(p.value, env);
|
|
11256
|
+
if (value === undefined) value = null; // <- force null instead of undefined
|
|
11257
|
+
}
|
|
11258
|
+
|
|
11259
|
+
out[key] = value;
|
|
11155
11260
|
}
|
|
11156
11261
|
return out;
|
|
11157
11262
|
}
|
|
11158
11263
|
|
|
11159
11264
|
|
|
11160
11265
|
|
|
11266
|
+
|
|
11161
11267
|
async evalMember(node, env) {
|
|
11162
11268
|
const obj = await this.evaluate(node.object, env);
|
|
11163
11269
|
|
|
@@ -11197,7 +11303,7 @@ async evalUpdate(node, env) {
|
|
|
11197
11303
|
|
|
11198
11304
|
}
|
|
11199
11305
|
|
|
11200
|
-
module.exports = Evaluator;
|
|
11306
|
+
module.exports = Evaluator;
|
|
11201
11307
|
|
|
11202
11308
|
/***/ }),
|
|
11203
11309
|
|
|
@@ -11692,14 +11798,18 @@ ifStatement() {
|
|
|
11692
11798
|
test = this.expression();
|
|
11693
11799
|
}
|
|
11694
11800
|
|
|
11695
|
-
const consequent = this.
|
|
11801
|
+
const consequent = this.statementOrBlock();
|
|
11696
11802
|
|
|
11697
11803
|
let alternate = null;
|
|
11698
11804
|
if (this.current.type === 'ELSE') {
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11805
|
+
this.eat('ELSE');
|
|
11806
|
+
if (this.current.type === 'IF') {
|
|
11807
|
+
alternate = this.ifStatement();
|
|
11808
|
+
} else {
|
|
11809
|
+
alternate = this.statementOrBlock();
|
|
11702
11810
|
}
|
|
11811
|
+
}
|
|
11812
|
+
|
|
11703
11813
|
|
|
11704
11814
|
return {
|
|
11705
11815
|
type: 'IfStatement',
|
|
@@ -11933,6 +12043,12 @@ returnStatement() {
|
|
|
11933
12043
|
|
|
11934
12044
|
return { type: 'ReturnStatement', argument, line: t.line, column: t.column };
|
|
11935
12045
|
}
|
|
12046
|
+
statementOrBlock() {
|
|
12047
|
+
if (this.current.type === 'LBRACE') {
|
|
12048
|
+
return this.block();
|
|
12049
|
+
}
|
|
12050
|
+
return this.statement();
|
|
12051
|
+
}
|
|
11936
12052
|
|
|
11937
12053
|
block() {
|
|
11938
12054
|
const t = this.current; // LBRACE token
|
|
@@ -12142,13 +12258,7 @@ postfix() {
|
|
|
12142
12258
|
continue;
|
|
12143
12259
|
}
|
|
12144
12260
|
|
|
12145
|
-
|
|
12146
|
-
const argNode = { type: 'Identifier', name: t.value, line: t.line, column: t.column };
|
|
12147
|
-
this.eat('IDENTIFIER');
|
|
12148
|
-
node = { type: 'CallExpression', callee: node, arguments: [argNode], line: node.line, column: node.column };
|
|
12149
|
-
continue;
|
|
12150
|
-
}
|
|
12151
|
-
|
|
12261
|
+
|
|
12152
12262
|
break;
|
|
12153
12263
|
}
|
|
12154
12264
|
return node;
|
|
@@ -12302,21 +12412,56 @@ arrowFunction(params) {
|
|
|
12302
12412
|
}
|
|
12303
12413
|
|
|
12304
12414
|
if (t.type === 'LBRACE') {
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12415
|
+
const startLine = t.line;
|
|
12416
|
+
const startCol = t.column;
|
|
12417
|
+
this.eat('LBRACE');
|
|
12418
|
+
|
|
12419
|
+
const props = [];
|
|
12420
|
+
|
|
12421
|
+
while (this.current.type !== 'RBRACE') {
|
|
12422
|
+
let key;
|
|
12423
|
+
|
|
12424
|
+
if (this.current.type === 'IDENTIFIER') {
|
|
12425
|
+
const k = this.current;
|
|
12426
|
+
this.eat('IDENTIFIER');
|
|
12427
|
+
key = {
|
|
12428
|
+
type: 'Literal',
|
|
12429
|
+
value: k.value,
|
|
12430
|
+
line: k.line,
|
|
12431
|
+
column: k.column
|
|
12432
|
+
};
|
|
12315
12433
|
}
|
|
12316
|
-
this.
|
|
12317
|
-
|
|
12434
|
+
else if (this.current.type === 'STRING') {
|
|
12435
|
+
const k = this.current;
|
|
12436
|
+
this.eat('STRING');
|
|
12437
|
+
key = {
|
|
12438
|
+
type: 'Literal',
|
|
12439
|
+
value: k.value,
|
|
12440
|
+
line: k.line,
|
|
12441
|
+
column: k.column
|
|
12442
|
+
};
|
|
12443
|
+
}
|
|
12444
|
+
else {
|
|
12445
|
+
throw new ParseError(
|
|
12446
|
+
'Invalid object key',
|
|
12447
|
+
this.current,
|
|
12448
|
+
this.source
|
|
12449
|
+
);
|
|
12450
|
+
}
|
|
12451
|
+
|
|
12452
|
+
this.eat('COLON');
|
|
12453
|
+
const value = this.expression();
|
|
12454
|
+
|
|
12455
|
+
props.push({ key, value });
|
|
12456
|
+
|
|
12457
|
+
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
12318
12458
|
}
|
|
12319
12459
|
|
|
12460
|
+
this.eat('RBRACE');
|
|
12461
|
+
return { type: 'ObjectExpression', props, line: startLine, column: startCol };
|
|
12462
|
+
}
|
|
12463
|
+
|
|
12464
|
+
|
|
12320
12465
|
throw new ParseError(
|
|
12321
12466
|
`Unexpected token '${t.type}'`,
|
|
12322
12467
|
t,
|
|
@@ -12507,7 +12652,7 @@ const Lexer = __nccwpck_require__(211);
|
|
|
12507
12652
|
const Parser = __nccwpck_require__(222);
|
|
12508
12653
|
const Evaluator = __nccwpck_require__(112);
|
|
12509
12654
|
|
|
12510
|
-
const VERSION = '1.1.
|
|
12655
|
+
const VERSION = '1.1.10';
|
|
12511
12656
|
|
|
12512
12657
|
const COLOR = {
|
|
12513
12658
|
reset: '\x1b[0m',
|
package/package.json
CHANGED
package/src/evaluator.js
CHANGED
|
@@ -20,7 +20,13 @@ class RuntimeError extends Error {
|
|
|
20
20
|
const lines = source.split('\n');
|
|
21
21
|
const srcLine = lines[node.line - 1] || '';
|
|
22
22
|
output += ` ${srcLine}\n`;
|
|
23
|
-
|
|
23
|
+
const caretPos =
|
|
24
|
+
typeof column === 'number' && column > 0
|
|
25
|
+
? column - 1
|
|
26
|
+
: 0;
|
|
27
|
+
|
|
28
|
+
output += ` ${' '.repeat(caretPos)}^\n`;
|
|
29
|
+
;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
super(output);
|
|
@@ -82,6 +88,34 @@ class Evaluator {
|
|
|
82
88
|
this.global = new Environment();
|
|
83
89
|
this.setupBuiltins();
|
|
84
90
|
}
|
|
91
|
+
async callFunction(fn, args, env, node = null) {
|
|
92
|
+
if (typeof fn === 'function') {
|
|
93
|
+
const val = await fn(...args);
|
|
94
|
+
return val === undefined ? null : val; // <<< enforce null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (fn && typeof fn === 'object' && fn.body && fn.params) {
|
|
98
|
+
const callEnv = new Environment(fn.env);
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < fn.params.length; i++) {
|
|
101
|
+
const param = fn.params[i];
|
|
102
|
+
const name = typeof param === 'string' ? param : param.name;
|
|
103
|
+
callEnv.define(name, args[i]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const val = await this.evaluate(fn.body, callEnv);
|
|
108
|
+
return val === undefined ? null : val; // <<< enforce null
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (e instanceof ReturnValue) return e.value === undefined ? null : e.value; // <<< enforce null
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new RuntimeError('Value is not callable', node, this.source);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
85
119
|
suggest(name, env) {
|
|
86
120
|
const names = new Set();
|
|
87
121
|
let current = env;
|
|
@@ -155,6 +189,7 @@ formatValue(value, seen = new Set()) {
|
|
|
155
189
|
|
|
156
190
|
|
|
157
191
|
setupBuiltins() {
|
|
192
|
+
const evaluator = this;
|
|
158
193
|
this.global.define('len', arg => {
|
|
159
194
|
if (Array.isArray(arg) || typeof arg === 'string') return arg.length;
|
|
160
195
|
if (arg && typeof arg === 'object') return Object.keys(arg).length;
|
|
@@ -166,63 +201,76 @@ formatValue(value, seen = new Set()) {
|
|
|
166
201
|
if (Array.isArray(arg)) return 'array';
|
|
167
202
|
return typeof arg;
|
|
168
203
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (typeof fn !== 'function') {
|
|
174
|
-
throw new RuntimeError('map() expects a function', null, this.source);
|
|
175
|
-
}
|
|
204
|
+
this.global.define('map', async (array, fn) => {
|
|
205
|
+
if (!Array.isArray(array)) {
|
|
206
|
+
throw new RuntimeError('map() expects an array', null, evaluator.source);
|
|
207
|
+
}
|
|
176
208
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
209
|
+
const result = [];
|
|
210
|
+
for (let i = 0; i < array.length; i++) {
|
|
211
|
+
result.push(
|
|
212
|
+
await evaluator.callFunction(
|
|
213
|
+
fn,
|
|
214
|
+
[array[i], i, array],
|
|
215
|
+
evaluator.global
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
});
|
|
190
221
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
result.push(array[i]);
|
|
222
|
+
this.global.define('filter', async (array, fn) => {
|
|
223
|
+
if (!Array.isArray(array)) {
|
|
224
|
+
throw new RuntimeError('filter() expects an array', null, evaluator.source);
|
|
195
225
|
}
|
|
196
|
-
}
|
|
197
|
-
return result;
|
|
198
|
-
});
|
|
199
|
-
this.global.define('reduce', async (array, fn, initial) => {
|
|
200
|
-
if (!Array.isArray(array)) {
|
|
201
|
-
throw new RuntimeError('reduce() expects an array', null, this.source);
|
|
202
|
-
}
|
|
203
|
-
if (typeof fn !== 'function') {
|
|
204
|
-
throw new RuntimeError('reduce() expects a function', null, this.source);
|
|
205
|
-
}
|
|
206
226
|
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
const result = [];
|
|
228
|
+
for (let i = 0; i < array.length; i++) {
|
|
229
|
+
if (
|
|
230
|
+
await evaluator.callFunction(
|
|
231
|
+
fn,
|
|
232
|
+
[array[i], i, array],
|
|
233
|
+
evaluator.global
|
|
234
|
+
)
|
|
235
|
+
) {
|
|
236
|
+
result.push(array[i]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
this.global.define('reduce', async (array, fn, initial) => {
|
|
243
|
+
if (!Array.isArray(array)) {
|
|
244
|
+
throw new RuntimeError('reduce() expects an array', null, evaluator.source);
|
|
245
|
+
}
|
|
209
246
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
247
|
+
let acc;
|
|
248
|
+
let i = 0;
|
|
249
|
+
|
|
250
|
+
if (initial !== undefined) {
|
|
251
|
+
acc = initial;
|
|
252
|
+
} else {
|
|
253
|
+
if (array.length === 0) {
|
|
254
|
+
throw new RuntimeError(
|
|
255
|
+
'reduce() of empty array with no initial value',
|
|
256
|
+
null,
|
|
257
|
+
evaluator.source
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
acc = array[0];
|
|
261
|
+
i = 1;
|
|
215
262
|
}
|
|
216
|
-
acc = array[0];
|
|
217
|
-
startIndex = 1;
|
|
218
|
-
}
|
|
219
263
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
264
|
+
for (; i < array.length; i++) {
|
|
265
|
+
acc = await evaluator.callFunction(
|
|
266
|
+
fn,
|
|
267
|
+
[acc, array[i], i, array],
|
|
268
|
+
evaluator.global
|
|
269
|
+
);
|
|
270
|
+
}
|
|
223
271
|
|
|
224
|
-
|
|
225
|
-
});
|
|
272
|
+
return acc;
|
|
273
|
+
});
|
|
226
274
|
|
|
227
275
|
this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
|
|
228
276
|
this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
|
|
@@ -267,12 +315,26 @@ this.global.define('range', (...args) => {
|
|
|
267
315
|
return readlineSync.question(prompt + ' ');
|
|
268
316
|
});
|
|
269
317
|
this.global.define('num', arg => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
318
|
+
if (arg === null || arg === undefined) return 0;
|
|
319
|
+
|
|
320
|
+
const t = typeof arg;
|
|
321
|
+
|
|
322
|
+
if (t === 'number') return arg; // already a number
|
|
323
|
+
if (t === 'boolean') return arg ? 1 : 0;
|
|
324
|
+
if (t === 'string') {
|
|
325
|
+
const n = Number(arg);
|
|
326
|
+
if (!Number.isNaN(n)) return n; // numeric string
|
|
327
|
+
return 0; // non-numeric string becomes 0
|
|
273
328
|
}
|
|
274
|
-
return
|
|
329
|
+
if (Array.isArray(arg)) return arg.length; // array → length
|
|
330
|
+
if (t === 'object') return Object.keys(arg).length; // object → number of keys
|
|
331
|
+
|
|
332
|
+
// fallback for anything else
|
|
333
|
+
const n = Number(arg);
|
|
334
|
+
if (!Number.isNaN(n)) return n;
|
|
335
|
+
return 0;
|
|
275
336
|
});
|
|
337
|
+
|
|
276
338
|
this.global.define('fetch', async (url, options = {}) => {
|
|
277
339
|
const res = await fetch(url, options);
|
|
278
340
|
return {
|
|
@@ -362,21 +424,30 @@ case 'RaceClause':
|
|
|
362
424
|
const callee = await this.evaluate(node.callee, env);
|
|
363
425
|
|
|
364
426
|
if (typeof callee === 'object' && callee.body) {
|
|
365
|
-
const evaluator = this;
|
|
366
|
-
|
|
427
|
+
const evaluator = this;
|
|
428
|
+
|
|
429
|
+
const Constructor = function (...args) {
|
|
367
430
|
const newEnv = new Environment(callee.env);
|
|
368
431
|
newEnv.define('this', this);
|
|
432
|
+
|
|
369
433
|
for (let i = 0; i < callee.params.length; i++) {
|
|
370
|
-
|
|
434
|
+
const param = callee.params[i];
|
|
435
|
+
const paramName = typeof param === 'string' ? param : param.name;
|
|
436
|
+
newEnv.define(paramName, args[i]);
|
|
371
437
|
}
|
|
372
|
-
|
|
438
|
+
|
|
439
|
+
return (async () => {
|
|
440
|
+
await evaluator.evaluate(callee.body, newEnv);
|
|
441
|
+
return this;
|
|
442
|
+
})();
|
|
373
443
|
};
|
|
374
444
|
|
|
375
445
|
const args = [];
|
|
376
446
|
for (const a of node.arguments) args.push(await this.evaluate(a, env));
|
|
377
|
-
return new Constructor(...args);
|
|
447
|
+
return await new Constructor(...args);
|
|
378
448
|
}
|
|
379
449
|
|
|
450
|
+
// native JS constructor fallback
|
|
380
451
|
if (typeof callee !== 'function') {
|
|
381
452
|
throw new RuntimeError('NewExpression callee is not a function', node, this.source);
|
|
382
453
|
}
|
|
@@ -598,27 +669,31 @@ evalArrowFunction(node, env) {
|
|
|
598
669
|
return async function (...args) {
|
|
599
670
|
const localEnv = new Environment(env);
|
|
600
671
|
|
|
672
|
+
// Bind parameters safely
|
|
601
673
|
node.params.forEach((p, i) => {
|
|
602
|
-
|
|
674
|
+
const paramName = typeof p === 'string' ? p : p.name;
|
|
675
|
+
localEnv.define(paramName, args[i]);
|
|
603
676
|
});
|
|
604
677
|
|
|
605
678
|
try {
|
|
606
679
|
if (node.isBlock) {
|
|
680
|
+
// Block body
|
|
607
681
|
const result = await evaluator.evaluate(node.body, localEnv);
|
|
608
|
-
return result
|
|
682
|
+
return result === undefined ? null : result; // ensure null instead of undefined
|
|
609
683
|
} else {
|
|
610
|
-
|
|
684
|
+
// Expression body
|
|
685
|
+
const result = await evaluator.evaluate(node.body, localEnv);
|
|
686
|
+
return result === undefined ? null : result; // ensure null instead of undefined
|
|
611
687
|
}
|
|
612
688
|
} catch (err) {
|
|
613
|
-
if (err instanceof ReturnValue)
|
|
614
|
-
return err.value;
|
|
615
|
-
}
|
|
689
|
+
if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
|
|
616
690
|
throw err;
|
|
617
691
|
}
|
|
618
692
|
};
|
|
619
693
|
}
|
|
620
694
|
|
|
621
695
|
|
|
696
|
+
|
|
622
697
|
async evalAssignment(node, env) {
|
|
623
698
|
const rightVal = await this.evaluate(node.right, env);
|
|
624
699
|
const left = node.left;
|
|
@@ -714,7 +789,30 @@ async evalBinary(node, env) {
|
|
|
714
789
|
}
|
|
715
790
|
|
|
716
791
|
switch (node.operator) {
|
|
717
|
-
case 'PLUS':
|
|
792
|
+
case 'PLUS': {
|
|
793
|
+
if (Array.isArray(l) && Array.isArray(r)) {
|
|
794
|
+
return l.concat(r);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (typeof l === 'string' || typeof r === 'string') {
|
|
798
|
+
return String(l) + String(r);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (typeof l === 'number' && typeof r === 'number') {
|
|
802
|
+
return l + r;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (typeof l === 'object' && typeof r === 'object') {
|
|
806
|
+
return { ...l, ...r };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
throw new RuntimeError(
|
|
810
|
+
`Unsupported operands for +: ${typeof l} and ${typeof r}`,
|
|
811
|
+
node,
|
|
812
|
+
this.source
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
718
816
|
case 'MINUS': return l - r;
|
|
719
817
|
case 'STAR': return l * r;
|
|
720
818
|
case 'SLASH': return l / r;
|
|
@@ -936,18 +1034,26 @@ async evalIndex(node, env) {
|
|
|
936
1034
|
async evalObject(node, env) {
|
|
937
1035
|
const out = {};
|
|
938
1036
|
for (const p of node.props) {
|
|
939
|
-
if (!p.key
|
|
940
|
-
throw new RuntimeError('
|
|
1037
|
+
if (!p.key) {
|
|
1038
|
+
throw new RuntimeError('Object property must have a key', node, this.source);
|
|
941
1039
|
}
|
|
1040
|
+
|
|
942
1041
|
const key = await this.evaluate(p.key, env);
|
|
943
|
-
|
|
944
|
-
|
|
1042
|
+
let value = null;
|
|
1043
|
+
|
|
1044
|
+
if (p.value) {
|
|
1045
|
+
value = await this.evaluate(p.value, env);
|
|
1046
|
+
if (value === undefined) value = null; // <- force null instead of undefined
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
out[key] = value;
|
|
945
1050
|
}
|
|
946
1051
|
return out;
|
|
947
1052
|
}
|
|
948
1053
|
|
|
949
1054
|
|
|
950
1055
|
|
|
1056
|
+
|
|
951
1057
|
async evalMember(node, env) {
|
|
952
1058
|
const obj = await this.evaluate(node.object, env);
|
|
953
1059
|
|
|
@@ -987,4 +1093,4 @@ async evalUpdate(node, env) {
|
|
|
987
1093
|
|
|
988
1094
|
}
|
|
989
1095
|
|
|
990
|
-
module.exports = Evaluator;
|
|
1096
|
+
module.exports = Evaluator;
|
package/src/parser.js
CHANGED
|
@@ -256,14 +256,18 @@ ifStatement() {
|
|
|
256
256
|
test = this.expression();
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
const consequent = this.
|
|
259
|
+
const consequent = this.statementOrBlock();
|
|
260
260
|
|
|
261
261
|
let alternate = null;
|
|
262
262
|
if (this.current.type === 'ELSE') {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
this.eat('ELSE');
|
|
264
|
+
if (this.current.type === 'IF') {
|
|
265
|
+
alternate = this.ifStatement();
|
|
266
|
+
} else {
|
|
267
|
+
alternate = this.statementOrBlock();
|
|
266
268
|
}
|
|
269
|
+
}
|
|
270
|
+
|
|
267
271
|
|
|
268
272
|
return {
|
|
269
273
|
type: 'IfStatement',
|
|
@@ -497,6 +501,12 @@ returnStatement() {
|
|
|
497
501
|
|
|
498
502
|
return { type: 'ReturnStatement', argument, line: t.line, column: t.column };
|
|
499
503
|
}
|
|
504
|
+
statementOrBlock() {
|
|
505
|
+
if (this.current.type === 'LBRACE') {
|
|
506
|
+
return this.block();
|
|
507
|
+
}
|
|
508
|
+
return this.statement();
|
|
509
|
+
}
|
|
500
510
|
|
|
501
511
|
block() {
|
|
502
512
|
const t = this.current; // LBRACE token
|
|
@@ -706,13 +716,7 @@ postfix() {
|
|
|
706
716
|
continue;
|
|
707
717
|
}
|
|
708
718
|
|
|
709
|
-
|
|
710
|
-
const argNode = { type: 'Identifier', name: t.value, line: t.line, column: t.column };
|
|
711
|
-
this.eat('IDENTIFIER');
|
|
712
|
-
node = { type: 'CallExpression', callee: node, arguments: [argNode], line: node.line, column: node.column };
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
|
|
719
|
+
|
|
716
720
|
break;
|
|
717
721
|
}
|
|
718
722
|
return node;
|
|
@@ -866,21 +870,56 @@ arrowFunction(params) {
|
|
|
866
870
|
}
|
|
867
871
|
|
|
868
872
|
if (t.type === 'LBRACE') {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
873
|
+
const startLine = t.line;
|
|
874
|
+
const startCol = t.column;
|
|
875
|
+
this.eat('LBRACE');
|
|
876
|
+
|
|
877
|
+
const props = [];
|
|
878
|
+
|
|
879
|
+
while (this.current.type !== 'RBRACE') {
|
|
880
|
+
let key;
|
|
881
|
+
|
|
882
|
+
if (this.current.type === 'IDENTIFIER') {
|
|
883
|
+
const k = this.current;
|
|
884
|
+
this.eat('IDENTIFIER');
|
|
885
|
+
key = {
|
|
886
|
+
type: 'Literal',
|
|
887
|
+
value: k.value,
|
|
888
|
+
line: k.line,
|
|
889
|
+
column: k.column
|
|
890
|
+
};
|
|
879
891
|
}
|
|
880
|
-
this.
|
|
881
|
-
|
|
892
|
+
else if (this.current.type === 'STRING') {
|
|
893
|
+
const k = this.current;
|
|
894
|
+
this.eat('STRING');
|
|
895
|
+
key = {
|
|
896
|
+
type: 'Literal',
|
|
897
|
+
value: k.value,
|
|
898
|
+
line: k.line,
|
|
899
|
+
column: k.column
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
throw new ParseError(
|
|
904
|
+
'Invalid object key',
|
|
905
|
+
this.current,
|
|
906
|
+
this.source
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
this.eat('COLON');
|
|
911
|
+
const value = this.expression();
|
|
912
|
+
|
|
913
|
+
props.push({ key, value });
|
|
914
|
+
|
|
915
|
+
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
882
916
|
}
|
|
883
917
|
|
|
918
|
+
this.eat('RBRACE');
|
|
919
|
+
return { type: 'ObjectExpression', props, line: startLine, column: startCol };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
|
|
884
923
|
throw new ParseError(
|
|
885
924
|
`Unexpected token '${t.type}'`,
|
|
886
925
|
t,
|