starlight-cli 1.0.46 → 1.0.47
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 +158 -52
- package/package.json +1 -1
- package/src/evaluator.js +65 -27
- package/src/lexer.js +33 -8
- package/src/parser.js +46 -12
- package/src/starlight.js +14 -5
package/dist/index.js
CHANGED
|
@@ -1352,6 +1352,14 @@ class ReturnValue {
|
|
|
1352
1352
|
}
|
|
1353
1353
|
class BreakSignal {}
|
|
1354
1354
|
class ContinueSignal {}
|
|
1355
|
+
class RuntimeError extends Error {
|
|
1356
|
+
constructor(message, node) {
|
|
1357
|
+
const line = node?.line ?? '?';
|
|
1358
|
+
const column = node?.column ?? '?';
|
|
1359
|
+
super(`RuntimeError: ${message} at line ${line}, column ${column}`);
|
|
1360
|
+
this.name = 'RuntimeError';
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1355
1363
|
|
|
1356
1364
|
class Environment {
|
|
1357
1365
|
constructor(parent = null) {
|
|
@@ -1368,7 +1376,7 @@ class Environment {
|
|
|
1368
1376
|
get(name) {
|
|
1369
1377
|
if (name in this.store) return this.store[name];
|
|
1370
1378
|
if (this.parent) return this.parent.get(name);
|
|
1371
|
-
throw new
|
|
1379
|
+
throw new RuntimeError(`Undefined variable: ${name}`);
|
|
1372
1380
|
}
|
|
1373
1381
|
|
|
1374
1382
|
set(name, value) {
|
|
@@ -1472,11 +1480,11 @@ this.global.define('range', (...args) => {
|
|
|
1472
1480
|
end = Number(args[1]);
|
|
1473
1481
|
step = Number(args[2]);
|
|
1474
1482
|
} else {
|
|
1475
|
-
throw new
|
|
1483
|
+
throw new RuntimeError('range() expects 1 to 3 arguments');
|
|
1476
1484
|
}
|
|
1477
1485
|
|
|
1478
1486
|
if (step === 0) {
|
|
1479
|
-
throw new
|
|
1487
|
+
throw new RuntimeError('range() step cannot be 0');
|
|
1480
1488
|
}
|
|
1481
1489
|
|
|
1482
1490
|
const result = [];
|
|
@@ -1500,7 +1508,7 @@ this.global.define('range', (...args) => {
|
|
|
1500
1508
|
this.global.define('num', arg => {
|
|
1501
1509
|
const n = Number(arg);
|
|
1502
1510
|
if (Number.isNaN(n)) {
|
|
1503
|
-
throw new
|
|
1511
|
+
throw new RuntimeError('Cannot convert value to number');
|
|
1504
1512
|
}
|
|
1505
1513
|
return n;
|
|
1506
1514
|
});
|
|
@@ -1611,7 +1619,7 @@ case 'DoTrackStatement':
|
|
|
1611
1619
|
}
|
|
1612
1620
|
|
|
1613
1621
|
if (typeof callee !== 'function') {
|
|
1614
|
-
throw new
|
|
1622
|
+
throw new RuntimeError('NewExpression callee is not a function', node);
|
|
1615
1623
|
}
|
|
1616
1624
|
|
|
1617
1625
|
const args = [];
|
|
@@ -1620,7 +1628,8 @@ case 'DoTrackStatement':
|
|
|
1620
1628
|
}
|
|
1621
1629
|
|
|
1622
1630
|
default:
|
|
1623
|
-
|
|
1631
|
+
throw new RuntimeError(`Unknown node type in evaluator: ${node.type}`, node);
|
|
1632
|
+
|
|
1624
1633
|
}
|
|
1625
1634
|
}
|
|
1626
1635
|
|
|
@@ -1636,15 +1645,26 @@ async evalDoTrack(node, env) {
|
|
|
1636
1645
|
try {
|
|
1637
1646
|
return await this.evaluate(node.body, env);
|
|
1638
1647
|
} catch (err) {
|
|
1639
|
-
if (!node.handler)
|
|
1648
|
+
if (!node.handler) {
|
|
1649
|
+
// Wrap any raw error into RuntimeError with line info
|
|
1650
|
+
if (err instanceof RuntimeError) throw err;
|
|
1651
|
+
throw new RuntimeError(err.message || 'Error in doTrack body', node.body);
|
|
1652
|
+
}
|
|
1640
1653
|
|
|
1641
1654
|
const trackEnv = new Environment(env);
|
|
1642
1655
|
trackEnv.define('error', err);
|
|
1643
1656
|
|
|
1644
|
-
|
|
1657
|
+
try {
|
|
1658
|
+
return await this.evaluate(node.handler, trackEnv);
|
|
1659
|
+
} catch (handlerErr) {
|
|
1660
|
+
// Wrap handler errors as well
|
|
1661
|
+
if (handlerErr instanceof RuntimeError) throw handlerErr;
|
|
1662
|
+
throw new RuntimeError(handlerErr.message || 'Error in doTrack handler', node.handler);
|
|
1663
|
+
}
|
|
1645
1664
|
}
|
|
1646
1665
|
}
|
|
1647
1666
|
|
|
1667
|
+
|
|
1648
1668
|
async evalImport(node, env) {
|
|
1649
1669
|
const spec = node.path;
|
|
1650
1670
|
let lib;
|
|
@@ -1660,7 +1680,7 @@ async evalImport(node, env) {
|
|
|
1660
1680
|
: path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
|
|
1661
1681
|
|
|
1662
1682
|
if (!fs.existsSync(fullPath)) {
|
|
1663
|
-
throw new
|
|
1683
|
+
throw new RuntimeError(`Import not found: ${spec}`, node);
|
|
1664
1684
|
}
|
|
1665
1685
|
|
|
1666
1686
|
const code = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -1687,7 +1707,7 @@ async evalImport(node, env) {
|
|
|
1687
1707
|
}
|
|
1688
1708
|
if (imp.type === 'NamedImport') {
|
|
1689
1709
|
if (!(imp.imported in lib)) {
|
|
1690
|
-
throw new
|
|
1710
|
+
throw new RuntimeError(`Module '${spec}' has no export '${imp.imported}'`, node);
|
|
1691
1711
|
}
|
|
1692
1712
|
env.define(imp.local, lib[imp.imported]);
|
|
1693
1713
|
}
|
|
@@ -1703,12 +1723,14 @@ async evalBlock(node, env) {
|
|
|
1703
1723
|
result = await this.evaluate(stmt, env);
|
|
1704
1724
|
} catch (e) {
|
|
1705
1725
|
if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
|
|
1706
|
-
|
|
1726
|
+
// Wrap any other error in RuntimeError with the current block node
|
|
1727
|
+
throw new RuntimeError(e.message || 'Error in block', stmt);
|
|
1707
1728
|
}
|
|
1708
1729
|
}
|
|
1709
1730
|
return result;
|
|
1710
1731
|
}
|
|
1711
1732
|
|
|
1733
|
+
|
|
1712
1734
|
async evalVarDeclaration(node, env) {
|
|
1713
1735
|
const val = await this.evaluate(node.expr, env);
|
|
1714
1736
|
return env.define(node.id, val);
|
|
@@ -1741,7 +1763,8 @@ async evalAssignment(node, env) {
|
|
|
1741
1763
|
return rightVal;
|
|
1742
1764
|
}
|
|
1743
1765
|
|
|
1744
|
-
|
|
1766
|
+
throw new RuntimeError('Invalid assignment target', node);
|
|
1767
|
+
|
|
1745
1768
|
}
|
|
1746
1769
|
|
|
1747
1770
|
async evalCompoundAssignment(node, env) {
|
|
@@ -1751,7 +1774,8 @@ async evalCompoundAssignment(node, env) {
|
|
|
1751
1774
|
if (left.type === 'Identifier') current = env.get(left.name);
|
|
1752
1775
|
else if (left.type === 'MemberExpression') current = await this.evalMember(left, env);
|
|
1753
1776
|
else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env);
|
|
1754
|
-
else throw new
|
|
1777
|
+
else throw new RuntimeError('Invalid compound assignment target', node);
|
|
1778
|
+
|
|
1755
1779
|
|
|
1756
1780
|
const rhs = await this.evaluate(node.right, env);
|
|
1757
1781
|
let computed;
|
|
@@ -1761,7 +1785,8 @@ async evalCompoundAssignment(node, env) {
|
|
|
1761
1785
|
case 'STAREQ': computed = current * rhs; break;
|
|
1762
1786
|
case 'SLASHEQ': computed = current / rhs; break;
|
|
1763
1787
|
case 'MODEQ': computed = current % rhs; break;
|
|
1764
|
-
default: throw new
|
|
1788
|
+
default: throw new RuntimeError(`Unknown compound operator: ${node.operator}`, node);
|
|
1789
|
+
|
|
1765
1790
|
}
|
|
1766
1791
|
|
|
1767
1792
|
if (left.type === 'Identifier') env.set(left.name, computed);
|
|
@@ -1794,7 +1819,7 @@ async evalBinary(node, env) {
|
|
|
1794
1819
|
const r = await this.evaluate(node.right, env);
|
|
1795
1820
|
|
|
1796
1821
|
if (node.operator === 'SLASH' && r === 0) {
|
|
1797
|
-
throw new
|
|
1822
|
+
throw new RuntimeError('Division by zero', node);
|
|
1798
1823
|
}
|
|
1799
1824
|
|
|
1800
1825
|
switch (node.operator) {
|
|
@@ -1809,7 +1834,8 @@ async evalBinary(node, env) {
|
|
|
1809
1834
|
case 'LTE': return l <= r;
|
|
1810
1835
|
case 'GT': return l > r;
|
|
1811
1836
|
case 'GTE': return l >= r;
|
|
1812
|
-
default: throw new
|
|
1837
|
+
default: throw new RuntimeError(`Unknown binary operator: ${node.operator}`, node);
|
|
1838
|
+
|
|
1813
1839
|
}
|
|
1814
1840
|
}
|
|
1815
1841
|
|
|
@@ -1817,7 +1843,8 @@ async evalLogical(node, env) {
|
|
|
1817
1843
|
const l = await this.evaluate(node.left, env);
|
|
1818
1844
|
if (node.operator === 'AND') return l && await this.evaluate(node.right, env);
|
|
1819
1845
|
if (node.operator === 'OR') return l || await this.evaluate(node.right, env);
|
|
1820
|
-
throw new
|
|
1846
|
+
throw new RuntimeError(`Unknown logical operator: ${node.operator}`, node);
|
|
1847
|
+
|
|
1821
1848
|
}
|
|
1822
1849
|
|
|
1823
1850
|
async evalUnary(node, env) {
|
|
@@ -1826,7 +1853,8 @@ async evalUnary(node, env) {
|
|
|
1826
1853
|
case 'NOT': return !val;
|
|
1827
1854
|
case 'MINUS': return -val;
|
|
1828
1855
|
case 'PLUS': return +val;
|
|
1829
|
-
default: throw new
|
|
1856
|
+
default: throw new RuntimeError(`Unknown unary operator: ${node.operator}`, node);
|
|
1857
|
+
|
|
1830
1858
|
}
|
|
1831
1859
|
}
|
|
1832
1860
|
|
|
@@ -1856,7 +1884,7 @@ async evalFor(node, env) {
|
|
|
1856
1884
|
const iterable = await this.evaluate(node.iterable, env);
|
|
1857
1885
|
|
|
1858
1886
|
if (iterable == null || typeof iterable !== 'object') {
|
|
1859
|
-
throw new
|
|
1887
|
+
throw new RuntimeError('Cannot iterate over non-iterable', node);
|
|
1860
1888
|
}
|
|
1861
1889
|
|
|
1862
1890
|
const loopVar = node.variable; // STRING from parser
|
|
@@ -1936,7 +1964,7 @@ async evalCall(node, env) {
|
|
|
1936
1964
|
return calleeEvaluated(...args);
|
|
1937
1965
|
}
|
|
1938
1966
|
if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
|
|
1939
|
-
throw new
|
|
1967
|
+
throw new RuntimeError('Call to non-function', node);
|
|
1940
1968
|
}
|
|
1941
1969
|
|
|
1942
1970
|
const fn = calleeEvaluated;
|
|
@@ -1960,12 +1988,13 @@ async evalIndex(node, env) {
|
|
|
1960
1988
|
const obj = await this.evaluate(node.object, env);
|
|
1961
1989
|
const idx = await this.evaluate(node.indexer, env);
|
|
1962
1990
|
|
|
1963
|
-
if (obj == null) throw new
|
|
1991
|
+
if (obj == null) throw new RuntimeError('Indexing null or undefined', node);
|
|
1992
|
+
|
|
1964
1993
|
if (Array.isArray(obj) && (idx < 0 || idx >= obj.length)) {
|
|
1965
|
-
throw new
|
|
1994
|
+
throw new RuntimeError('Array index out of bounds', node);
|
|
1966
1995
|
}
|
|
1967
1996
|
if (typeof obj === 'object' && !(idx in obj)) {
|
|
1968
|
-
throw new
|
|
1997
|
+
throw new RuntimeError(`Property '${idx}' does not exist`, node);
|
|
1969
1998
|
}
|
|
1970
1999
|
|
|
1971
2000
|
return obj[idx];
|
|
@@ -1973,14 +2002,22 @@ async evalIndex(node, env) {
|
|
|
1973
2002
|
|
|
1974
2003
|
async evalObject(node, env) {
|
|
1975
2004
|
const out = {};
|
|
1976
|
-
for (const p of node.props)
|
|
2005
|
+
for (const p of node.props) {
|
|
2006
|
+
if (!p.key || !p.value) {
|
|
2007
|
+
throw new RuntimeError('Invalid object property', node);
|
|
2008
|
+
}
|
|
2009
|
+
const key = await this.evaluate(p.key, env);
|
|
2010
|
+
const value = await this.evaluate(p.value, env);
|
|
2011
|
+
out[key] = value;
|
|
2012
|
+
}
|
|
1977
2013
|
return out;
|
|
1978
2014
|
}
|
|
1979
2015
|
|
|
2016
|
+
|
|
1980
2017
|
async evalMember(node, env) {
|
|
1981
2018
|
const obj = await this.evaluate(node.object, env);
|
|
1982
|
-
|
|
1983
|
-
|
|
2019
|
+
if (obj == null) throw new RuntimeError('Member access of null or undefined', node);
|
|
2020
|
+
if (!(node.property in obj)) throw new RuntimeError(`Property '${node.property}' does not exist`, node);
|
|
1984
2021
|
return obj[node.property];
|
|
1985
2022
|
}
|
|
1986
2023
|
|
|
@@ -1990,7 +2027,8 @@ async evalUpdate(node, env) {
|
|
|
1990
2027
|
if (arg.type === 'Identifier') return env.get(arg.name);
|
|
1991
2028
|
if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
|
|
1992
2029
|
if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
|
|
1993
|
-
throw new
|
|
2030
|
+
throw new RuntimeError('Invalid update target', node);
|
|
2031
|
+
|
|
1994
2032
|
};
|
|
1995
2033
|
const setValue = async (v) => {
|
|
1996
2034
|
if (arg.type === 'Identifier') env.set(arg.name, v);
|
|
@@ -2020,14 +2058,33 @@ module.exports = Evaluator;
|
|
|
2020
2058
|
/***/ 211:
|
|
2021
2059
|
/***/ ((module) => {
|
|
2022
2060
|
|
|
2061
|
+
class LexerError extends Error {
|
|
2062
|
+
constructor(message, line, column, source = '') {
|
|
2063
|
+
let output = `SyntaxError: ${message}\n`;
|
|
2064
|
+
output += ` at line ${line}, column ${column}\n`;
|
|
2065
|
+
|
|
2066
|
+
if (source && line != null) {
|
|
2067
|
+
const lines = source.split('\n');
|
|
2068
|
+
const srcLine = lines[line - 1] || '';
|
|
2069
|
+
output += ` ${srcLine}\n`;
|
|
2070
|
+
output += ` ${' '.repeat(column - 1)}^\n`;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
super(output);
|
|
2074
|
+
this.name = 'SyntaxError';
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2023
2078
|
class Lexer {
|
|
2024
2079
|
constructor(input) {
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2080
|
+
this.input = input;
|
|
2081
|
+
this.source = input;
|
|
2082
|
+
this.pos = 0;
|
|
2083
|
+
this.currentChar = input[0] || null;
|
|
2084
|
+
this.line = 1;
|
|
2085
|
+
this.column = 1;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2031
2088
|
|
|
2032
2089
|
advance() {
|
|
2033
2090
|
if (this.currentChar === '\n') {
|
|
@@ -2045,8 +2102,14 @@ class Lexer {
|
|
|
2045
2102
|
}
|
|
2046
2103
|
|
|
2047
2104
|
error(msg) {
|
|
2048
|
-
|
|
2049
|
-
|
|
2105
|
+
throw new LexerError(
|
|
2106
|
+
msg,
|
|
2107
|
+
this.line,
|
|
2108
|
+
this.column,
|
|
2109
|
+
this.source
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2050
2113
|
|
|
2051
2114
|
skipWhitespace() {
|
|
2052
2115
|
while (this.currentChar && /\s/.test(this.currentChar)) this.advance();
|
|
@@ -2214,25 +2277,54 @@ module.exports = Lexer;
|
|
|
2214
2277
|
/***/ 222:
|
|
2215
2278
|
/***/ ((module) => {
|
|
2216
2279
|
|
|
2217
|
-
class
|
|
2218
|
-
constructor(
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2280
|
+
class ParseError extends Error {
|
|
2281
|
+
constructor(message, token, source) {
|
|
2282
|
+
const line = token?.line ?? '?';
|
|
2283
|
+
const column = token?.column ?? '?';
|
|
2284
|
+
|
|
2285
|
+
let output = `SyntaxError: ${message}\n`;
|
|
2286
|
+
|
|
2287
|
+
if (source && token?.line != null) {
|
|
2288
|
+
const lines = source.split('\n');
|
|
2289
|
+
const srcLine = lines[token.line - 1] || '';
|
|
2290
|
+
output += ` at line ${line}, column ${column}\n`;
|
|
2291
|
+
output += ` ${srcLine}\n`;
|
|
2292
|
+
output += ` ${' '.repeat(column - 1)}^\n`;
|
|
2293
|
+
} else {
|
|
2294
|
+
output += ` at line ${line}, column ${column}\n`;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
super(output);
|
|
2298
|
+
this.name = 'SyntaxError';
|
|
2222
2299
|
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
class Parser {
|
|
2303
|
+
constructor(tokens, source = '') {
|
|
2304
|
+
this.tokens = tokens;
|
|
2305
|
+
this.source = source;
|
|
2306
|
+
this.pos = 0;
|
|
2307
|
+
this.current = this.tokens[this.pos];
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2223
2310
|
|
|
2224
2311
|
advance() {
|
|
2225
2312
|
this.pos++;
|
|
2226
2313
|
this.current = this.pos < this.tokens.length ? this.tokens[this.pos] : { type: 'EOF' };
|
|
2227
2314
|
}
|
|
2228
2315
|
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2316
|
+
eat(type) {
|
|
2317
|
+
if (this.current.type === type) {
|
|
2318
|
+
this.advance();
|
|
2319
|
+
} else {
|
|
2320
|
+
throw new ParseError(
|
|
2321
|
+
`Expected '${type}' but got '${this.current.type}'`,
|
|
2322
|
+
this.current,
|
|
2323
|
+
this.source
|
|
2324
|
+
);
|
|
2235
2325
|
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2236
2328
|
|
|
2237
2329
|
peekType(offset = 1) {
|
|
2238
2330
|
return (this.tokens[this.pos + offset] || { type: 'EOF' }).type;
|
|
@@ -2848,7 +2940,12 @@ arrowFunction(params) {
|
|
|
2848
2940
|
return { type: 'ObjectExpression', props };
|
|
2849
2941
|
}
|
|
2850
2942
|
|
|
2851
|
-
throw new
|
|
2943
|
+
throw new ParseError(
|
|
2944
|
+
`Unexpected token '${t.type}'`,
|
|
2945
|
+
t,
|
|
2946
|
+
this.source
|
|
2947
|
+
);
|
|
2948
|
+
|
|
2852
2949
|
}
|
|
2853
2950
|
|
|
2854
2951
|
}
|
|
@@ -2955,7 +3052,7 @@ const Lexer = __nccwpck_require__(211);
|
|
|
2955
3052
|
const Parser = __nccwpck_require__(222);
|
|
2956
3053
|
const Evaluator = __nccwpck_require__(112);
|
|
2957
3054
|
|
|
2958
|
-
const VERSION = '1.0.
|
|
3055
|
+
const VERSION = '1.0.47';
|
|
2959
3056
|
|
|
2960
3057
|
const COLOR = {
|
|
2961
3058
|
reset: '\x1b[0m',
|
|
@@ -3123,19 +3220,28 @@ function runFile(filePath, isTemp = false, callback) {
|
|
|
3123
3220
|
|
|
3124
3221
|
try {
|
|
3125
3222
|
const lexer = new Lexer(code);
|
|
3126
|
-
const
|
|
3223
|
+
const tokens = lexer.getTokens();
|
|
3224
|
+
const parser = new Parser(tokens);
|
|
3127
3225
|
const ast = parser.parse();
|
|
3128
3226
|
new Evaluator().evaluate(ast);
|
|
3129
3227
|
} catch (e) {
|
|
3130
|
-
|
|
3228
|
+
if (e.name === 'RuntimeError') {
|
|
3229
|
+
console.error(COLOR.red + e.message + COLOR.reset);
|
|
3230
|
+
} else if (e instanceof SyntaxError) {
|
|
3231
|
+
console.error(COLOR.red + `SyntaxError: ${e.message}` + COLOR.reset);
|
|
3232
|
+
} else {
|
|
3233
|
+
console.error(COLOR.red + `Error: ${e.message || e}` + COLOR.reset);
|
|
3234
|
+
}
|
|
3131
3235
|
return waitAndExit(1);
|
|
3132
3236
|
}
|
|
3133
3237
|
|
|
3134
|
-
|
|
3135
3238
|
if (callback) callback();
|
|
3136
|
-
if (isTemp)
|
|
3239
|
+
if (isTemp) {
|
|
3240
|
+
try { fs.unlinkSync(filePath); } catch {}
|
|
3241
|
+
}
|
|
3137
3242
|
}
|
|
3138
3243
|
|
|
3244
|
+
|
|
3139
3245
|
if (!args[0].startsWith('--')) {
|
|
3140
3246
|
runFile(path.resolve(args[0]));
|
|
3141
3247
|
}
|
package/package.json
CHANGED
package/src/evaluator.js
CHANGED
|
@@ -9,6 +9,14 @@ class ReturnValue {
|
|
|
9
9
|
}
|
|
10
10
|
class BreakSignal {}
|
|
11
11
|
class ContinueSignal {}
|
|
12
|
+
class RuntimeError extends Error {
|
|
13
|
+
constructor(message, node) {
|
|
14
|
+
const line = node?.line ?? '?';
|
|
15
|
+
const column = node?.column ?? '?';
|
|
16
|
+
super(`RuntimeError: ${message} at line ${line}, column ${column}`);
|
|
17
|
+
this.name = 'RuntimeError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
12
20
|
|
|
13
21
|
class Environment {
|
|
14
22
|
constructor(parent = null) {
|
|
@@ -25,7 +33,7 @@ class Environment {
|
|
|
25
33
|
get(name) {
|
|
26
34
|
if (name in this.store) return this.store[name];
|
|
27
35
|
if (this.parent) return this.parent.get(name);
|
|
28
|
-
throw new
|
|
36
|
+
throw new RuntimeError(`Undefined variable: ${name}`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
set(name, value) {
|
|
@@ -129,11 +137,11 @@ this.global.define('range', (...args) => {
|
|
|
129
137
|
end = Number(args[1]);
|
|
130
138
|
step = Number(args[2]);
|
|
131
139
|
} else {
|
|
132
|
-
throw new
|
|
140
|
+
throw new RuntimeError('range() expects 1 to 3 arguments');
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
if (step === 0) {
|
|
136
|
-
throw new
|
|
144
|
+
throw new RuntimeError('range() step cannot be 0');
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
const result = [];
|
|
@@ -157,7 +165,7 @@ this.global.define('range', (...args) => {
|
|
|
157
165
|
this.global.define('num', arg => {
|
|
158
166
|
const n = Number(arg);
|
|
159
167
|
if (Number.isNaN(n)) {
|
|
160
|
-
throw new
|
|
168
|
+
throw new RuntimeError('Cannot convert value to number');
|
|
161
169
|
}
|
|
162
170
|
return n;
|
|
163
171
|
});
|
|
@@ -268,7 +276,7 @@ case 'DoTrackStatement':
|
|
|
268
276
|
}
|
|
269
277
|
|
|
270
278
|
if (typeof callee !== 'function') {
|
|
271
|
-
throw new
|
|
279
|
+
throw new RuntimeError('NewExpression callee is not a function', node);
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
const args = [];
|
|
@@ -277,7 +285,8 @@ case 'DoTrackStatement':
|
|
|
277
285
|
}
|
|
278
286
|
|
|
279
287
|
default:
|
|
280
|
-
|
|
288
|
+
throw new RuntimeError(`Unknown node type in evaluator: ${node.type}`, node);
|
|
289
|
+
|
|
281
290
|
}
|
|
282
291
|
}
|
|
283
292
|
|
|
@@ -293,15 +302,26 @@ async evalDoTrack(node, env) {
|
|
|
293
302
|
try {
|
|
294
303
|
return await this.evaluate(node.body, env);
|
|
295
304
|
} catch (err) {
|
|
296
|
-
if (!node.handler)
|
|
305
|
+
if (!node.handler) {
|
|
306
|
+
// Wrap any raw error into RuntimeError with line info
|
|
307
|
+
if (err instanceof RuntimeError) throw err;
|
|
308
|
+
throw new RuntimeError(err.message || 'Error in doTrack body', node.body);
|
|
309
|
+
}
|
|
297
310
|
|
|
298
311
|
const trackEnv = new Environment(env);
|
|
299
312
|
trackEnv.define('error', err);
|
|
300
313
|
|
|
301
|
-
|
|
314
|
+
try {
|
|
315
|
+
return await this.evaluate(node.handler, trackEnv);
|
|
316
|
+
} catch (handlerErr) {
|
|
317
|
+
// Wrap handler errors as well
|
|
318
|
+
if (handlerErr instanceof RuntimeError) throw handlerErr;
|
|
319
|
+
throw new RuntimeError(handlerErr.message || 'Error in doTrack handler', node.handler);
|
|
320
|
+
}
|
|
302
321
|
}
|
|
303
322
|
}
|
|
304
323
|
|
|
324
|
+
|
|
305
325
|
async evalImport(node, env) {
|
|
306
326
|
const spec = node.path;
|
|
307
327
|
let lib;
|
|
@@ -317,7 +337,7 @@ async evalImport(node, env) {
|
|
|
317
337
|
: path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
|
|
318
338
|
|
|
319
339
|
if (!fs.existsSync(fullPath)) {
|
|
320
|
-
throw new
|
|
340
|
+
throw new RuntimeError(`Import not found: ${spec}`, node);
|
|
321
341
|
}
|
|
322
342
|
|
|
323
343
|
const code = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -344,7 +364,7 @@ async evalImport(node, env) {
|
|
|
344
364
|
}
|
|
345
365
|
if (imp.type === 'NamedImport') {
|
|
346
366
|
if (!(imp.imported in lib)) {
|
|
347
|
-
throw new
|
|
367
|
+
throw new RuntimeError(`Module '${spec}' has no export '${imp.imported}'`, node);
|
|
348
368
|
}
|
|
349
369
|
env.define(imp.local, lib[imp.imported]);
|
|
350
370
|
}
|
|
@@ -360,12 +380,14 @@ async evalBlock(node, env) {
|
|
|
360
380
|
result = await this.evaluate(stmt, env);
|
|
361
381
|
} catch (e) {
|
|
362
382
|
if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
|
|
363
|
-
|
|
383
|
+
// Wrap any other error in RuntimeError with the current block node
|
|
384
|
+
throw new RuntimeError(e.message || 'Error in block', stmt);
|
|
364
385
|
}
|
|
365
386
|
}
|
|
366
387
|
return result;
|
|
367
388
|
}
|
|
368
389
|
|
|
390
|
+
|
|
369
391
|
async evalVarDeclaration(node, env) {
|
|
370
392
|
const val = await this.evaluate(node.expr, env);
|
|
371
393
|
return env.define(node.id, val);
|
|
@@ -398,7 +420,8 @@ async evalAssignment(node, env) {
|
|
|
398
420
|
return rightVal;
|
|
399
421
|
}
|
|
400
422
|
|
|
401
|
-
|
|
423
|
+
throw new RuntimeError('Invalid assignment target', node);
|
|
424
|
+
|
|
402
425
|
}
|
|
403
426
|
|
|
404
427
|
async evalCompoundAssignment(node, env) {
|
|
@@ -408,7 +431,8 @@ async evalCompoundAssignment(node, env) {
|
|
|
408
431
|
if (left.type === 'Identifier') current = env.get(left.name);
|
|
409
432
|
else if (left.type === 'MemberExpression') current = await this.evalMember(left, env);
|
|
410
433
|
else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env);
|
|
411
|
-
else throw new
|
|
434
|
+
else throw new RuntimeError('Invalid compound assignment target', node);
|
|
435
|
+
|
|
412
436
|
|
|
413
437
|
const rhs = await this.evaluate(node.right, env);
|
|
414
438
|
let computed;
|
|
@@ -418,7 +442,8 @@ async evalCompoundAssignment(node, env) {
|
|
|
418
442
|
case 'STAREQ': computed = current * rhs; break;
|
|
419
443
|
case 'SLASHEQ': computed = current / rhs; break;
|
|
420
444
|
case 'MODEQ': computed = current % rhs; break;
|
|
421
|
-
default: throw new
|
|
445
|
+
default: throw new RuntimeError(`Unknown compound operator: ${node.operator}`, node);
|
|
446
|
+
|
|
422
447
|
}
|
|
423
448
|
|
|
424
449
|
if (left.type === 'Identifier') env.set(left.name, computed);
|
|
@@ -451,7 +476,7 @@ async evalBinary(node, env) {
|
|
|
451
476
|
const r = await this.evaluate(node.right, env);
|
|
452
477
|
|
|
453
478
|
if (node.operator === 'SLASH' && r === 0) {
|
|
454
|
-
throw new
|
|
479
|
+
throw new RuntimeError('Division by zero', node);
|
|
455
480
|
}
|
|
456
481
|
|
|
457
482
|
switch (node.operator) {
|
|
@@ -466,7 +491,8 @@ async evalBinary(node, env) {
|
|
|
466
491
|
case 'LTE': return l <= r;
|
|
467
492
|
case 'GT': return l > r;
|
|
468
493
|
case 'GTE': return l >= r;
|
|
469
|
-
default: throw new
|
|
494
|
+
default: throw new RuntimeError(`Unknown binary operator: ${node.operator}`, node);
|
|
495
|
+
|
|
470
496
|
}
|
|
471
497
|
}
|
|
472
498
|
|
|
@@ -474,7 +500,8 @@ async evalLogical(node, env) {
|
|
|
474
500
|
const l = await this.evaluate(node.left, env);
|
|
475
501
|
if (node.operator === 'AND') return l && await this.evaluate(node.right, env);
|
|
476
502
|
if (node.operator === 'OR') return l || await this.evaluate(node.right, env);
|
|
477
|
-
throw new
|
|
503
|
+
throw new RuntimeError(`Unknown logical operator: ${node.operator}`, node);
|
|
504
|
+
|
|
478
505
|
}
|
|
479
506
|
|
|
480
507
|
async evalUnary(node, env) {
|
|
@@ -483,7 +510,8 @@ async evalUnary(node, env) {
|
|
|
483
510
|
case 'NOT': return !val;
|
|
484
511
|
case 'MINUS': return -val;
|
|
485
512
|
case 'PLUS': return +val;
|
|
486
|
-
default: throw new
|
|
513
|
+
default: throw new RuntimeError(`Unknown unary operator: ${node.operator}`, node);
|
|
514
|
+
|
|
487
515
|
}
|
|
488
516
|
}
|
|
489
517
|
|
|
@@ -513,7 +541,7 @@ async evalFor(node, env) {
|
|
|
513
541
|
const iterable = await this.evaluate(node.iterable, env);
|
|
514
542
|
|
|
515
543
|
if (iterable == null || typeof iterable !== 'object') {
|
|
516
|
-
throw new
|
|
544
|
+
throw new RuntimeError('Cannot iterate over non-iterable', node);
|
|
517
545
|
}
|
|
518
546
|
|
|
519
547
|
const loopVar = node.variable; // STRING from parser
|
|
@@ -593,7 +621,7 @@ async evalCall(node, env) {
|
|
|
593
621
|
return calleeEvaluated(...args);
|
|
594
622
|
}
|
|
595
623
|
if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
|
|
596
|
-
throw new
|
|
624
|
+
throw new RuntimeError('Call to non-function', node);
|
|
597
625
|
}
|
|
598
626
|
|
|
599
627
|
const fn = calleeEvaluated;
|
|
@@ -617,12 +645,13 @@ async evalIndex(node, env) {
|
|
|
617
645
|
const obj = await this.evaluate(node.object, env);
|
|
618
646
|
const idx = await this.evaluate(node.indexer, env);
|
|
619
647
|
|
|
620
|
-
if (obj == null) throw new
|
|
648
|
+
if (obj == null) throw new RuntimeError('Indexing null or undefined', node);
|
|
649
|
+
|
|
621
650
|
if (Array.isArray(obj) && (idx < 0 || idx >= obj.length)) {
|
|
622
|
-
throw new
|
|
651
|
+
throw new RuntimeError('Array index out of bounds', node);
|
|
623
652
|
}
|
|
624
653
|
if (typeof obj === 'object' && !(idx in obj)) {
|
|
625
|
-
throw new
|
|
654
|
+
throw new RuntimeError(`Property '${idx}' does not exist`, node);
|
|
626
655
|
}
|
|
627
656
|
|
|
628
657
|
return obj[idx];
|
|
@@ -630,14 +659,22 @@ async evalIndex(node, env) {
|
|
|
630
659
|
|
|
631
660
|
async evalObject(node, env) {
|
|
632
661
|
const out = {};
|
|
633
|
-
for (const p of node.props)
|
|
662
|
+
for (const p of node.props) {
|
|
663
|
+
if (!p.key || !p.value) {
|
|
664
|
+
throw new RuntimeError('Invalid object property', node);
|
|
665
|
+
}
|
|
666
|
+
const key = await this.evaluate(p.key, env);
|
|
667
|
+
const value = await this.evaluate(p.value, env);
|
|
668
|
+
out[key] = value;
|
|
669
|
+
}
|
|
634
670
|
return out;
|
|
635
671
|
}
|
|
636
672
|
|
|
673
|
+
|
|
637
674
|
async evalMember(node, env) {
|
|
638
675
|
const obj = await this.evaluate(node.object, env);
|
|
639
|
-
|
|
640
|
-
|
|
676
|
+
if (obj == null) throw new RuntimeError('Member access of null or undefined', node);
|
|
677
|
+
if (!(node.property in obj)) throw new RuntimeError(`Property '${node.property}' does not exist`, node);
|
|
641
678
|
return obj[node.property];
|
|
642
679
|
}
|
|
643
680
|
|
|
@@ -647,7 +684,8 @@ async evalUpdate(node, env) {
|
|
|
647
684
|
if (arg.type === 'Identifier') return env.get(arg.name);
|
|
648
685
|
if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
|
|
649
686
|
if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
|
|
650
|
-
throw new
|
|
687
|
+
throw new RuntimeError('Invalid update target', node);
|
|
688
|
+
|
|
651
689
|
};
|
|
652
690
|
const setValue = async (v) => {
|
|
653
691
|
if (arg.type === 'Identifier') env.set(arg.name, v);
|
package/src/lexer.js
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
|
+
class LexerError extends Error {
|
|
2
|
+
constructor(message, line, column, source = '') {
|
|
3
|
+
let output = `SyntaxError: ${message}\n`;
|
|
4
|
+
output += ` at line ${line}, column ${column}\n`;
|
|
5
|
+
|
|
6
|
+
if (source && line != null) {
|
|
7
|
+
const lines = source.split('\n');
|
|
8
|
+
const srcLine = lines[line - 1] || '';
|
|
9
|
+
output += ` ${srcLine}\n`;
|
|
10
|
+
output += ` ${' '.repeat(column - 1)}^\n`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
super(output);
|
|
14
|
+
this.name = 'SyntaxError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
1
18
|
class Lexer {
|
|
2
19
|
constructor(input) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
this.input = input;
|
|
21
|
+
this.source = input;
|
|
22
|
+
this.pos = 0;
|
|
23
|
+
this.currentChar = input[0] || null;
|
|
24
|
+
this.line = 1;
|
|
25
|
+
this.column = 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
|
|
10
29
|
advance() {
|
|
11
30
|
if (this.currentChar === '\n') {
|
|
@@ -23,8 +42,14 @@ class Lexer {
|
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
error(msg) {
|
|
26
|
-
|
|
27
|
-
|
|
45
|
+
throw new LexerError(
|
|
46
|
+
msg,
|
|
47
|
+
this.line,
|
|
48
|
+
this.column,
|
|
49
|
+
this.source
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
|
|
29
54
|
skipWhitespace() {
|
|
30
55
|
while (this.currentChar && /\s/.test(this.currentChar)) this.advance();
|
package/src/parser.js
CHANGED
|
@@ -1,22 +1,51 @@
|
|
|
1
|
-
class
|
|
2
|
-
constructor(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
class ParseError extends Error {
|
|
2
|
+
constructor(message, token, source) {
|
|
3
|
+
const line = token?.line ?? '?';
|
|
4
|
+
const column = token?.column ?? '?';
|
|
5
|
+
|
|
6
|
+
let output = `SyntaxError: ${message}\n`;
|
|
7
|
+
|
|
8
|
+
if (source && token?.line != null) {
|
|
9
|
+
const lines = source.split('\n');
|
|
10
|
+
const srcLine = lines[token.line - 1] || '';
|
|
11
|
+
output += ` at line ${line}, column ${column}\n`;
|
|
12
|
+
output += ` ${srcLine}\n`;
|
|
13
|
+
output += ` ${' '.repeat(column - 1)}^\n`;
|
|
14
|
+
} else {
|
|
15
|
+
output += ` at line ${line}, column ${column}\n`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
super(output);
|
|
19
|
+
this.name = 'SyntaxError';
|
|
6
20
|
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class Parser {
|
|
24
|
+
constructor(tokens, source = '') {
|
|
25
|
+
this.tokens = tokens;
|
|
26
|
+
this.source = source;
|
|
27
|
+
this.pos = 0;
|
|
28
|
+
this.current = this.tokens[this.pos];
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
|
|
8
32
|
advance() {
|
|
9
33
|
this.pos++;
|
|
10
34
|
this.current = this.pos < this.tokens.length ? this.tokens[this.pos] : { type: 'EOF' };
|
|
11
35
|
}
|
|
12
36
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
37
|
+
eat(type) {
|
|
38
|
+
if (this.current.type === type) {
|
|
39
|
+
this.advance();
|
|
40
|
+
} else {
|
|
41
|
+
throw new ParseError(
|
|
42
|
+
`Expected '${type}' but got '${this.current.type}'`,
|
|
43
|
+
this.current,
|
|
44
|
+
this.source
|
|
45
|
+
);
|
|
19
46
|
}
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
|
|
21
50
|
peekType(offset = 1) {
|
|
22
51
|
return (this.tokens[this.pos + offset] || { type: 'EOF' }).type;
|
|
@@ -632,7 +661,12 @@ arrowFunction(params) {
|
|
|
632
661
|
return { type: 'ObjectExpression', props };
|
|
633
662
|
}
|
|
634
663
|
|
|
635
|
-
throw new
|
|
664
|
+
throw new ParseError(
|
|
665
|
+
`Unexpected token '${t.type}'`,
|
|
666
|
+
t,
|
|
667
|
+
this.source
|
|
668
|
+
);
|
|
669
|
+
|
|
636
670
|
}
|
|
637
671
|
|
|
638
672
|
}
|
package/src/starlight.js
CHANGED
|
@@ -9,7 +9,7 @@ const Lexer = require('./lexer');
|
|
|
9
9
|
const Parser = require('./parser');
|
|
10
10
|
const Evaluator = require('./evaluator');
|
|
11
11
|
|
|
12
|
-
const VERSION = '1.0.
|
|
12
|
+
const VERSION = '1.0.47';
|
|
13
13
|
|
|
14
14
|
const COLOR = {
|
|
15
15
|
reset: '\x1b[0m',
|
|
@@ -177,19 +177,28 @@ function runFile(filePath, isTemp = false, callback) {
|
|
|
177
177
|
|
|
178
178
|
try {
|
|
179
179
|
const lexer = new Lexer(code);
|
|
180
|
-
const
|
|
180
|
+
const tokens = lexer.getTokens();
|
|
181
|
+
const parser = new Parser(tokens);
|
|
181
182
|
const ast = parser.parse();
|
|
182
183
|
new Evaluator().evaluate(ast);
|
|
183
184
|
} catch (e) {
|
|
184
|
-
|
|
185
|
+
if (e.name === 'RuntimeError') {
|
|
186
|
+
console.error(COLOR.red + e.message + COLOR.reset);
|
|
187
|
+
} else if (e instanceof SyntaxError) {
|
|
188
|
+
console.error(COLOR.red + `SyntaxError: ${e.message}` + COLOR.reset);
|
|
189
|
+
} else {
|
|
190
|
+
console.error(COLOR.red + `Error: ${e.message || e}` + COLOR.reset);
|
|
191
|
+
}
|
|
185
192
|
return waitAndExit(1);
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
|
|
189
195
|
if (callback) callback();
|
|
190
|
-
if (isTemp)
|
|
196
|
+
if (isTemp) {
|
|
197
|
+
try { fs.unlinkSync(filePath); } catch {}
|
|
198
|
+
}
|
|
191
199
|
}
|
|
192
200
|
|
|
201
|
+
|
|
193
202
|
if (!args[0].startsWith('--')) {
|
|
194
203
|
runFile(path.resolve(args[0]));
|
|
195
204
|
}
|