starlight-cli 1.0.17 → 1.0.19
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 +191 -143
- package/package.json +1 -1
- package/src/evaluator.js +71 -109
- package/src/lexer.js +69 -8
- package/src/parser.js +49 -27
- package/src/starlight.js +1 -1
package/dist/index.js
CHANGED
|
@@ -1347,9 +1347,7 @@ const Lexer = __nccwpck_require__(211);
|
|
|
1347
1347
|
const Parser = __nccwpck_require__(222);
|
|
1348
1348
|
const path = __nccwpck_require__(928);
|
|
1349
1349
|
|
|
1350
|
-
class ReturnValue {
|
|
1351
|
-
constructor(value) { this.value = value; }
|
|
1352
|
-
}
|
|
1350
|
+
class ReturnValue { constructor(value) { this.value = value; } }
|
|
1353
1351
|
class BreakSignal {}
|
|
1354
1352
|
class ContinueSignal {}
|
|
1355
1353
|
|
|
@@ -1392,37 +1390,27 @@ class Evaluator {
|
|
|
1392
1390
|
|
|
1393
1391
|
setupBuiltins() {
|
|
1394
1392
|
this.global.define('len', arg => {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1393
|
+
if (Array.isArray(arg) || typeof arg === 'string') return arg.length;
|
|
1394
|
+
if (arg && typeof arg === 'object') return Object.keys(arg).length;
|
|
1395
|
+
return 0;
|
|
1398
1396
|
});
|
|
1399
1397
|
|
|
1400
1398
|
this.global.define('print', arg => { console.log(arg); return null; });
|
|
1401
1399
|
this.global.define('type', arg => {
|
|
1402
|
-
|
|
1403
|
-
|
|
1400
|
+
if (Array.isArray(arg)) return 'array';
|
|
1401
|
+
return typeof arg;
|
|
1404
1402
|
});
|
|
1405
1403
|
this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
|
|
1406
1404
|
this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
|
|
1407
1405
|
|
|
1408
|
-
this.global.define('ask', prompt =>
|
|
1409
|
-
const readlineSync = __nccwpck_require__(552);
|
|
1410
|
-
return readlineSync.question(prompt + ' ');
|
|
1411
|
-
});
|
|
1406
|
+
this.global.define('ask', prompt => readlineSync.question(prompt + ' '));
|
|
1412
1407
|
this.global.define('num', arg => {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
this.global.define('str', arg => {
|
|
1421
|
-
return String(arg);
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1408
|
+
const n = Number(arg);
|
|
1409
|
+
if (Number.isNaN(n)) throw new Error('Cannot convert value to number');
|
|
1410
|
+
return n;
|
|
1411
|
+
});
|
|
1412
|
+
this.global.define('str', arg => String(arg));
|
|
1413
|
+
}
|
|
1426
1414
|
|
|
1427
1415
|
evaluate(node, env = this.global) {
|
|
1428
1416
|
switch (node.type) {
|
|
@@ -1439,6 +1427,7 @@ this.global.define('str', arg => {
|
|
|
1439
1427
|
case 'LogicalExpression': return this.evalLogical(node, env);
|
|
1440
1428
|
case 'UnaryExpression': return this.evalUnary(node, env);
|
|
1441
1429
|
case 'Literal': return node.value;
|
|
1430
|
+
case 'TemplateLiteral': return this.evalTemplateLiteral(node, env);
|
|
1442
1431
|
case 'Identifier': return env.get(node.name);
|
|
1443
1432
|
case 'IfStatement': return this.evalIf(node, env);
|
|
1444
1433
|
case 'WhileStatement': return this.evalWhile(node, env);
|
|
@@ -1464,72 +1453,65 @@ this.global.define('str', arg => {
|
|
|
1464
1453
|
|
|
1465
1454
|
evalProgram(node, env) {
|
|
1466
1455
|
let result = null;
|
|
1467
|
-
for (const stmt of node.body)
|
|
1468
|
-
result = this.evaluate(stmt, env);
|
|
1469
|
-
}
|
|
1456
|
+
for (const stmt of node.body) result = this.evaluate(stmt, env);
|
|
1470
1457
|
return result;
|
|
1471
1458
|
}
|
|
1472
|
-
evalImport(node, env) {
|
|
1473
|
-
const spec = node.path;
|
|
1474
|
-
let lib;
|
|
1475
1459
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
if (!fs.existsSync(fullPath)) {
|
|
1487
|
-
throw new Error(`Import not found: ${spec}`);
|
|
1460
|
+
evalTemplateLiteral(node, env) {
|
|
1461
|
+
// node.parts is an array from parser: Literal or expression nodes
|
|
1462
|
+
let result = '';
|
|
1463
|
+
for (const part of node.parts) {
|
|
1464
|
+
if (part.type === 'Literal') {
|
|
1465
|
+
result += part.value;
|
|
1466
|
+
} else {
|
|
1467
|
+
const val = this.evaluate(part, env);
|
|
1468
|
+
result += val != null ? val.toString() : '';
|
|
1488
1469
|
}
|
|
1470
|
+
}
|
|
1471
|
+
return result;
|
|
1472
|
+
}
|
|
1489
1473
|
|
|
1490
|
-
const code = fs.readFileSync(fullPath, 'utf-8');
|
|
1491
|
-
const tokens = new Lexer(code).getTokens();
|
|
1492
|
-
const ast = new Parser(tokens).parse();
|
|
1493
1474
|
|
|
1494
|
-
|
|
1495
|
-
|
|
1475
|
+
evalImport(node, env) {
|
|
1476
|
+
const spec = node.path;
|
|
1477
|
+
let lib;
|
|
1478
|
+
try {
|
|
1479
|
+
const resolved = require.resolve(spec, { paths: [process.cwd()] });
|
|
1480
|
+
lib = require(resolved);
|
|
1481
|
+
} catch (e) {
|
|
1482
|
+
const fullPath = path.isAbsolute(spec)
|
|
1483
|
+
? spec
|
|
1484
|
+
: path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
|
|
1496
1485
|
|
|
1497
|
-
|
|
1498
|
-
for (const key of Object.keys(moduleEnv.store)) {
|
|
1499
|
-
lib[key] = moduleEnv.store[key];
|
|
1500
|
-
}
|
|
1486
|
+
if (!fs.existsSync(fullPath)) throw new Error(`Import not found: ${spec}`);
|
|
1501
1487
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1488
|
+
const code = fs.readFileSync(fullPath, 'utf-8');
|
|
1489
|
+
const tokens = new Lexer(code).getTokens();
|
|
1490
|
+
const ast = new Parser(tokens).parse();
|
|
1491
|
+
const moduleEnv = new Environment(env);
|
|
1492
|
+
this.evaluate(ast, moduleEnv);
|
|
1504
1493
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
}
|
|
1509
|
-
if (imp.type === 'NamespaceImport') {
|
|
1510
|
-
env.define(imp.local, lib);
|
|
1494
|
+
lib = {};
|
|
1495
|
+
for (const key of Object.keys(moduleEnv.store)) lib[key] = moduleEnv.store[key];
|
|
1496
|
+
lib.default = lib;
|
|
1511
1497
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1498
|
+
|
|
1499
|
+
for (const imp of node.specifiers) {
|
|
1500
|
+
if (imp.type === 'DefaultImport') env.define(imp.local, lib.default ?? lib);
|
|
1501
|
+
if (imp.type === 'NamespaceImport') env.define(imp.local, lib);
|
|
1502
|
+
if (imp.type === 'NamedImport') {
|
|
1503
|
+
if (!(imp.imported in lib)) throw new Error(`Module '${spec}' has no export '${imp.imported}'`);
|
|
1504
|
+
env.define(imp.local, lib[imp.imported]);
|
|
1515
1505
|
}
|
|
1516
|
-
env.define(imp.local, lib[imp.imported]);
|
|
1517
1506
|
}
|
|
1507
|
+
return null;
|
|
1518
1508
|
}
|
|
1519
1509
|
|
|
1520
|
-
return null;
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
1510
|
evalBlock(node, env) {
|
|
1525
1511
|
let result = null;
|
|
1526
1512
|
for (const stmt of node.body) {
|
|
1527
|
-
try {
|
|
1528
|
-
|
|
1529
|
-
} catch (e) {
|
|
1530
|
-
if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
|
|
1531
|
-
throw e;
|
|
1532
|
-
}
|
|
1513
|
+
try { result = this.evaluate(stmt, env); }
|
|
1514
|
+
catch (e) { if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e; else throw e; }
|
|
1533
1515
|
}
|
|
1534
1516
|
return result;
|
|
1535
1517
|
}
|
|
@@ -1542,27 +1524,15 @@ evalImport(node, env) {
|
|
|
1542
1524
|
evalAssignment(node, env) {
|
|
1543
1525
|
const rightVal = this.evaluate(node.right, env);
|
|
1544
1526
|
const left = node.left;
|
|
1545
|
-
|
|
1546
1527
|
if (left.type === 'Identifier') return env.set(left.name, rightVal);
|
|
1547
|
-
if (left.type === 'MemberExpression') {
|
|
1548
|
-
|
|
1549
|
-
obj[left.property] = rightVal;
|
|
1550
|
-
return rightVal;
|
|
1551
|
-
}
|
|
1552
|
-
if (left.type === 'IndexExpression') {
|
|
1553
|
-
const obj = this.evaluate(left.object, env);
|
|
1554
|
-
const idx = this.evaluate(left.indexer, env);
|
|
1555
|
-
obj[idx] = rightVal;
|
|
1556
|
-
return rightVal;
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1528
|
+
if (left.type === 'MemberExpression') { const obj = this.evaluate(left.object, env); obj[left.property] = rightVal; return rightVal; }
|
|
1529
|
+
if (left.type === 'IndexExpression') { const obj = this.evaluate(left.object, env); const idx = this.evaluate(left.indexer, env); obj[idx] = rightVal; return rightVal; }
|
|
1559
1530
|
throw new Error('Invalid assignment target');
|
|
1560
1531
|
}
|
|
1561
1532
|
|
|
1562
1533
|
evalCompoundAssignment(node, env) {
|
|
1563
1534
|
const left = node.left;
|
|
1564
1535
|
let current;
|
|
1565
|
-
|
|
1566
1536
|
if (left.type === 'Identifier') current = env.get(left.name);
|
|
1567
1537
|
else if (left.type === 'MemberExpression') current = this.evalMember(left, env);
|
|
1568
1538
|
else if (left.type === 'IndexExpression') current = this.evalIndex(left, env);
|
|
@@ -1580,9 +1550,7 @@ evalImport(node, env) {
|
|
|
1580
1550
|
}
|
|
1581
1551
|
|
|
1582
1552
|
if (left.type === 'Identifier') env.set(left.name, computed);
|
|
1583
|
-
else if (left.type === 'MemberExpression') this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
|
|
1584
1553
|
else this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
|
|
1585
|
-
|
|
1586
1554
|
return computed;
|
|
1587
1555
|
}
|
|
1588
1556
|
|
|
@@ -1594,8 +1562,7 @@ evalImport(node, env) {
|
|
|
1594
1562
|
|
|
1595
1563
|
evalAsk(node, env) {
|
|
1596
1564
|
const prompt = this.evaluate(node.prompt, env);
|
|
1597
|
-
|
|
1598
|
-
return input;
|
|
1565
|
+
return readlineSync.question(prompt + ' ');
|
|
1599
1566
|
}
|
|
1600
1567
|
|
|
1601
1568
|
evalDefine(node, env) {
|
|
@@ -1612,12 +1579,16 @@ evalImport(node, env) {
|
|
|
1612
1579
|
case 'STAR': return l * r;
|
|
1613
1580
|
case 'SLASH': return l / r;
|
|
1614
1581
|
case 'MOD': return l % r;
|
|
1615
|
-
case 'EQEQ': return l
|
|
1616
|
-
case 'NOTEQ': return l
|
|
1582
|
+
case 'EQEQ': return l == r;
|
|
1583
|
+
case 'NOTEQ': return l != r;
|
|
1584
|
+
case 'STRICT_EQ': return l === r;
|
|
1585
|
+
case 'STRICT_NOTEQ': return l !== r;
|
|
1617
1586
|
case 'LT': return l < r;
|
|
1618
1587
|
case 'LTE': return l <= r;
|
|
1619
1588
|
case 'GT': return l > r;
|
|
1620
1589
|
case 'GTE': return l >= r;
|
|
1590
|
+
case 'LSHIFT': return l << r;
|
|
1591
|
+
case 'RSHIFT': return l >> r;
|
|
1621
1592
|
default: throw new Error(`Unknown binary operator ${node.operator}`);
|
|
1622
1593
|
}
|
|
1623
1594
|
}
|
|
@@ -1685,9 +1656,7 @@ evalImport(node, env) {
|
|
|
1685
1656
|
const args = node.arguments.map(a => this.evaluate(a, env));
|
|
1686
1657
|
return calleeEvaluated(...args);
|
|
1687
1658
|
}
|
|
1688
|
-
if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body)
|
|
1689
|
-
throw new Error('Call to non-function');
|
|
1690
|
-
}
|
|
1659
|
+
if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) throw new Error('Call to non-function');
|
|
1691
1660
|
const fn = calleeEvaluated;
|
|
1692
1661
|
const callEnv = new Environment(fn.env);
|
|
1693
1662
|
fn.params.forEach((p, i) => {
|
|
@@ -1727,15 +1696,8 @@ evalImport(node, env) {
|
|
|
1727
1696
|
};
|
|
1728
1697
|
const setValue = (v) => {
|
|
1729
1698
|
if (arg.type === 'Identifier') env.set(arg.name, v);
|
|
1730
|
-
else if (arg.type === 'MemberExpression') {
|
|
1731
|
-
|
|
1732
|
-
obj[arg.property] = v;
|
|
1733
|
-
}
|
|
1734
|
-
else if (arg.type === 'IndexExpression') {
|
|
1735
|
-
const obj = this.evaluate(arg.object, env);
|
|
1736
|
-
const idx = this.evaluate(arg.indexer, env);
|
|
1737
|
-
obj[idx] = v;
|
|
1738
|
-
}
|
|
1699
|
+
else if (arg.type === 'MemberExpression') { const obj = this.evaluate(arg.object, env); obj[arg.property] = v; }
|
|
1700
|
+
else if (arg.type === 'IndexExpression') { const obj = this.evaluate(arg.object, env); const idx = this.evaluate(arg.indexer, env); obj[idx] = v; }
|
|
1739
1701
|
};
|
|
1740
1702
|
const current = getCurrent();
|
|
1741
1703
|
const newVal = (node.operator === 'PLUSPLUS') ? current + 1 : current - 1;
|
|
@@ -1744,7 +1706,8 @@ evalImport(node, env) {
|
|
|
1744
1706
|
}
|
|
1745
1707
|
}
|
|
1746
1708
|
|
|
1747
|
-
module.exports = Evaluator;
|
|
1709
|
+
module.exports = Evaluator;
|
|
1710
|
+
|
|
1748
1711
|
|
|
1749
1712
|
/***/ }),
|
|
1750
1713
|
|
|
@@ -1763,8 +1726,8 @@ class Lexer {
|
|
|
1763
1726
|
this.currentChar = this.pos < this.input.length ? this.input[this.pos] : null;
|
|
1764
1727
|
}
|
|
1765
1728
|
|
|
1766
|
-
peek() {
|
|
1767
|
-
return this.pos +
|
|
1729
|
+
peek(n = 1) {
|
|
1730
|
+
return this.pos + n < this.input.length ? this.input[this.pos + n] : null;
|
|
1768
1731
|
}
|
|
1769
1732
|
|
|
1770
1733
|
error(msg) {
|
|
@@ -1806,10 +1769,21 @@ class Lexer {
|
|
|
1806
1769
|
result += this.currentChar;
|
|
1807
1770
|
this.advance();
|
|
1808
1771
|
}
|
|
1809
|
-
return { type: 'NUMBER', value: parseFloat(result) };
|
|
1810
1772
|
}
|
|
1811
1773
|
|
|
1812
|
-
|
|
1774
|
+
if (this.currentChar && /[eE]/.test(this.currentChar)) {
|
|
1775
|
+
result += this.currentChar; this.advance();
|
|
1776
|
+
if (this.currentChar === '+' || this.currentChar === '-') {
|
|
1777
|
+
result += this.currentChar; this.advance();
|
|
1778
|
+
}
|
|
1779
|
+
if (!/[0-9]/.test(this.currentChar)) this.error("Invalid exponent in number");
|
|
1780
|
+
while (this.currentChar && /[0-9]/.test(this.currentChar)) {
|
|
1781
|
+
result += this.currentChar;
|
|
1782
|
+
this.advance();
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
return { type: 'NUMBER', value: parseFloat(result) };
|
|
1813
1787
|
}
|
|
1814
1788
|
|
|
1815
1789
|
identifier() {
|
|
@@ -1822,7 +1796,7 @@ class Lexer {
|
|
|
1822
1796
|
const keywords = [
|
|
1823
1797
|
'let', 'sldeploy', 'if', 'else', 'while', 'for',
|
|
1824
1798
|
'break', 'continue', 'func', 'return', 'true', 'false', 'null',
|
|
1825
|
-
'ask', 'define', 'import', 'from', 'as'
|
|
1799
|
+
'ask', 'define', 'import', 'from', 'as', 'undefined'
|
|
1826
1800
|
];
|
|
1827
1801
|
|
|
1828
1802
|
if (keywords.includes(result)) {
|
|
@@ -1830,8 +1804,7 @@ class Lexer {
|
|
|
1830
1804
|
}
|
|
1831
1805
|
|
|
1832
1806
|
return { type: 'IDENTIFIER', value: result };
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1807
|
+
}
|
|
1835
1808
|
|
|
1836
1809
|
string() {
|
|
1837
1810
|
const quote = this.currentChar;
|
|
@@ -1863,6 +1836,46 @@ class Lexer {
|
|
|
1863
1836
|
return { type: 'STRING', value: result };
|
|
1864
1837
|
}
|
|
1865
1838
|
|
|
1839
|
+
templateString() {
|
|
1840
|
+
this.advance(); // skip initial backtick `
|
|
1841
|
+
let buffer = '';
|
|
1842
|
+
const tokens = [];
|
|
1843
|
+
|
|
1844
|
+
while (this.currentChar && this.currentChar !== '`') {
|
|
1845
|
+
if (this.currentChar === '$' && this.peek() === '{') {
|
|
1846
|
+
if (buffer.length > 0) {
|
|
1847
|
+
tokens.push({ type: 'TEMPLATE_STRING', value: buffer });
|
|
1848
|
+
buffer = '';
|
|
1849
|
+
}
|
|
1850
|
+
tokens.push({ type: 'DOLLAR_LBRACE' }); // ${
|
|
1851
|
+
this.advance(); this.advance();
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
if (this.currentChar === '\\') {
|
|
1856
|
+
this.advance();
|
|
1857
|
+
switch (this.currentChar) {
|
|
1858
|
+
case 'n': buffer += '\n'; break;
|
|
1859
|
+
case 't': buffer += '\t'; break;
|
|
1860
|
+
case '`': buffer += '`'; break;
|
|
1861
|
+
case '\\': buffer += '\\'; break;
|
|
1862
|
+
default: buffer += this.currentChar;
|
|
1863
|
+
}
|
|
1864
|
+
} else {
|
|
1865
|
+
buffer += this.currentChar;
|
|
1866
|
+
}
|
|
1867
|
+
this.advance();
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (buffer.length > 0) tokens.push({ type: 'TEMPLATE_STRING', value: buffer });
|
|
1871
|
+
|
|
1872
|
+
if (this.currentChar !== '`') this.error('Unterminated template string');
|
|
1873
|
+
this.advance(); // skip closing backtick
|
|
1874
|
+
|
|
1875
|
+
return tokens; // return array of tokens
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
|
|
1866
1879
|
getTokens() {
|
|
1867
1880
|
const tokens = [];
|
|
1868
1881
|
|
|
@@ -1872,15 +1885,26 @@ class Lexer {
|
|
|
1872
1885
|
if (/[0-9]/.test(this.currentChar)) { tokens.push(this.number()); continue; }
|
|
1873
1886
|
if (/[A-Za-z_]/.test(this.currentChar)) { tokens.push(this.identifier()); continue; }
|
|
1874
1887
|
if (this.currentChar === '"' || this.currentChar === "'") { tokens.push(this.string()); continue; }
|
|
1888
|
+
if (this.currentChar === '`') {
|
|
1889
|
+
const tTokens = this.templateString();
|
|
1890
|
+
tokens.push(...tTokens); // push all tokens returned from templateString
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1875
1894
|
|
|
1876
1895
|
const char = this.currentChar;
|
|
1877
1896
|
const next = this.peek();
|
|
1897
|
+
const next2 = this.peek(2);
|
|
1878
1898
|
|
|
1899
|
+
if (char === '=' && next === '=' && next2 === '=') { tokens.push({ type: 'STRICT_EQ' }); this.advance(); this.advance(); this.advance(); continue; }
|
|
1900
|
+
if (char === '!' && next === '=' && next2 === '=') { tokens.push({ type: 'STRICT_NOTEQ' }); this.advance(); this.advance(); this.advance(); continue; }
|
|
1879
1901
|
if (char === '=' && next === '=') { tokens.push({ type: 'EQEQ' }); this.advance(); this.advance(); continue; }
|
|
1880
1902
|
if (char === '=' && next === '>') { tokens.push({ type: 'ARROW' }); this.advance(); this.advance(); continue; }
|
|
1881
1903
|
if (char === '!' && next === '=') { tokens.push({ type: 'NOTEQ' }); this.advance(); this.advance(); continue; }
|
|
1882
1904
|
if (char === '<' && next === '=') { tokens.push({ type: 'LTE' }); this.advance(); this.advance(); continue; }
|
|
1883
1905
|
if (char === '>' && next === '=') { tokens.push({ type: 'GTE' }); this.advance(); this.advance(); continue; }
|
|
1906
|
+
if (char === '<' && next === '<') { tokens.push({ type: 'LSHIFT' }); this.advance(); this.advance(); continue; }
|
|
1907
|
+
if (char === '>' && next === '>') { tokens.push({ type: 'RSHIFT' }); this.advance(); this.advance(); continue; }
|
|
1884
1908
|
if (char === '&' && next === '&') { tokens.push({ type: 'AND' }); this.advance(); this.advance(); continue; }
|
|
1885
1909
|
if (char === '|' && next === '|') { tokens.push({ type: 'OR' }); this.advance(); this.advance(); continue; }
|
|
1886
1910
|
if (char === '+' && next === '+') { tokens.push({ type: 'PLUSPLUS' }); this.advance(); this.advance(); continue; }
|
|
@@ -1907,7 +1931,8 @@ class Lexer {
|
|
|
1907
1931
|
}
|
|
1908
1932
|
}
|
|
1909
1933
|
|
|
1910
|
-
module.exports = Lexer;
|
|
1934
|
+
module.exports = Lexer;
|
|
1935
|
+
|
|
1911
1936
|
|
|
1912
1937
|
/***/ }),
|
|
1913
1938
|
|
|
@@ -2015,40 +2040,41 @@ class Parser {
|
|
|
2015
2040
|
const body = this.block();
|
|
2016
2041
|
return { type: 'WhileStatement', test, body };
|
|
2017
2042
|
}
|
|
2043
|
+
|
|
2018
2044
|
importStatement() {
|
|
2019
2045
|
this.eat('IMPORT');
|
|
2020
2046
|
|
|
2021
2047
|
let specifiers = [];
|
|
2022
2048
|
|
|
2023
2049
|
if (this.current.type === 'STAR') {
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2050
|
+
this.eat('STAR');
|
|
2051
|
+
this.eat('AS');
|
|
2052
|
+
const name = this.current.value;
|
|
2053
|
+
this.eat('IDENTIFIER');
|
|
2054
|
+
specifiers.push({ type: 'NamespaceImport', local: name });
|
|
2029
2055
|
}
|
|
2030
2056
|
else if (this.current.type === 'LBRACE') {
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
}
|
|
2041
|
-
specifiers.push({ type: 'NamedImport', imported: importedName, local: localName });
|
|
2042
|
-
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
2057
|
+
this.eat('LBRACE');
|
|
2058
|
+
while (this.current.type !== 'RBRACE') {
|
|
2059
|
+
const importedName = this.current.value;
|
|
2060
|
+
this.eat('IDENTIFIER');
|
|
2061
|
+
let localName = importedName;
|
|
2062
|
+
if (this.current.type === 'AS') {
|
|
2063
|
+
this.eat('AS');
|
|
2064
|
+
localName = this.current.value;
|
|
2065
|
+
this.eat('IDENTIFIER');
|
|
2043
2066
|
}
|
|
2044
|
-
|
|
2067
|
+
specifiers.push({ type: 'NamedImport', imported: importedName, local: localName });
|
|
2068
|
+
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
2069
|
+
}
|
|
2070
|
+
this.eat('RBRACE');
|
|
2045
2071
|
}
|
|
2046
2072
|
else if (this.current.type === 'IDENTIFIER') {
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2073
|
+
const localName = this.current.value;
|
|
2074
|
+
this.eat('IDENTIFIER');
|
|
2075
|
+
specifiers.push({ type: 'DefaultImport', local: localName });
|
|
2050
2076
|
} else {
|
|
2051
|
-
|
|
2077
|
+
throw new Error('Unexpected token in import statement');
|
|
2052
2078
|
}
|
|
2053
2079
|
|
|
2054
2080
|
this.eat('FROM');
|
|
@@ -2059,8 +2085,7 @@ class Parser {
|
|
|
2059
2085
|
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
2060
2086
|
|
|
2061
2087
|
return { type: 'ImportStatement', path: pathToken.value, specifiers };
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2088
|
+
}
|
|
2064
2089
|
|
|
2065
2090
|
forStatement() {
|
|
2066
2091
|
this.eat('FOR');
|
|
@@ -2140,6 +2165,23 @@ class Parser {
|
|
|
2140
2165
|
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
2141
2166
|
return { type: 'ExpressionStatement', expression: expr };
|
|
2142
2167
|
}
|
|
2168
|
+
parseTemplateLiteral() {
|
|
2169
|
+
const parts = [];
|
|
2170
|
+
while (true) {
|
|
2171
|
+
if (this.current.type === 'TEMPLATE_STRING') {
|
|
2172
|
+
parts.push({ type: 'Literal', value: this.current.value });
|
|
2173
|
+
this.eat('TEMPLATE_STRING');
|
|
2174
|
+
} else if (this.current.type === 'DOLLAR_LBRACE') {
|
|
2175
|
+
this.eat('DOLLAR_LBRACE');
|
|
2176
|
+
const expr = this.expression();
|
|
2177
|
+
parts.push(expr);
|
|
2178
|
+
this.eat('RBRACE');
|
|
2179
|
+
} else {
|
|
2180
|
+
break;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return { type: 'TemplateLiteral', parts };
|
|
2184
|
+
}
|
|
2143
2185
|
|
|
2144
2186
|
expression() {
|
|
2145
2187
|
return this.assignment();
|
|
@@ -2186,7 +2228,7 @@ class Parser {
|
|
|
2186
2228
|
|
|
2187
2229
|
equality() {
|
|
2188
2230
|
let node = this.comparison();
|
|
2189
|
-
while (['EQEQ', 'NOTEQ'].includes(this.current.type)) {
|
|
2231
|
+
while (['EQEQ', 'NOTEQ', 'STRICT_EQ', 'STRICT_NOTEQ'].includes(this.current.type)) {
|
|
2190
2232
|
const op = this.current.type;
|
|
2191
2233
|
this.eat(op);
|
|
2192
2234
|
node = { type: 'BinaryExpression', operator: op, left: node, right: this.comparison() };
|
|
@@ -2196,7 +2238,7 @@ class Parser {
|
|
|
2196
2238
|
|
|
2197
2239
|
comparison() {
|
|
2198
2240
|
let node = this.term();
|
|
2199
|
-
while (['LT', 'LTE', 'GT', 'GTE'].includes(this.current.type)) {
|
|
2241
|
+
while (['LT', 'LTE', 'GT', 'GTE', 'LSHIFT', 'RSHIFT'].includes(this.current.type)) {
|
|
2200
2242
|
const op = this.current.type;
|
|
2201
2243
|
this.eat(op);
|
|
2202
2244
|
node = { type: 'BinaryExpression', operator: op, left: node, right: this.term() };
|
|
@@ -2287,9 +2329,14 @@ class Parser {
|
|
|
2287
2329
|
|
|
2288
2330
|
if (t.type === 'NUMBER') { this.eat('NUMBER'); return { type: 'Literal', value: t.value }; }
|
|
2289
2331
|
if (t.type === 'STRING') { this.eat('STRING'); return { type: 'Literal', value: t.value }; }
|
|
2332
|
+
if (t.type === 'TEMPLATE_STRING' || t.type === 'DOLLAR_LBRACE') {
|
|
2333
|
+
return this.parseTemplateLiteral();
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2290
2336
|
if (t.type === 'TRUE') { this.eat('TRUE'); return { type: 'Literal', value: true }; }
|
|
2291
2337
|
if (t.type === 'FALSE') { this.eat('FALSE'); return { type: 'Literal', value: false }; }
|
|
2292
2338
|
if (t.type === 'NULL') { this.eat('NULL'); return { type: 'Literal', value: null }; }
|
|
2339
|
+
if (t.type === 'UNDEFINED') { this.eat('UNDEFINED'); return { type: 'Literal', value: undefined }; }
|
|
2293
2340
|
|
|
2294
2341
|
if (t.type === 'ASK') {
|
|
2295
2342
|
this.eat('ASK');
|
|
@@ -2354,7 +2401,8 @@ class Parser {
|
|
|
2354
2401
|
}
|
|
2355
2402
|
}
|
|
2356
2403
|
|
|
2357
|
-
module.exports = Parser;
|
|
2404
|
+
module.exports = Parser;
|
|
2405
|
+
|
|
2358
2406
|
|
|
2359
2407
|
/***/ }),
|
|
2360
2408
|
|
|
@@ -2456,7 +2504,7 @@ const Lexer = __nccwpck_require__(211);
|
|
|
2456
2504
|
const Parser = __nccwpck_require__(222);
|
|
2457
2505
|
const Evaluator = __nccwpck_require__(112);
|
|
2458
2506
|
|
|
2459
|
-
const VERSION = '1.0.
|
|
2507
|
+
const VERSION = '1.0.19';
|
|
2460
2508
|
|
|
2461
2509
|
const COLOR = {
|
|
2462
2510
|
reset: '\x1b[0m',
|
package/package.json
CHANGED
package/src/evaluator.js
CHANGED
|
@@ -4,9 +4,7 @@ const Lexer = require('./lexer');
|
|
|
4
4
|
const Parser = require('./parser');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
class ReturnValue {
|
|
8
|
-
constructor(value) { this.value = value; }
|
|
9
|
-
}
|
|
7
|
+
class ReturnValue { constructor(value) { this.value = value; } }
|
|
10
8
|
class BreakSignal {}
|
|
11
9
|
class ContinueSignal {}
|
|
12
10
|
|
|
@@ -49,37 +47,27 @@ class Evaluator {
|
|
|
49
47
|
|
|
50
48
|
setupBuiltins() {
|
|
51
49
|
this.global.define('len', arg => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
if (Array.isArray(arg) || typeof arg === 'string') return arg.length;
|
|
51
|
+
if (arg && typeof arg === 'object') return Object.keys(arg).length;
|
|
52
|
+
return 0;
|
|
55
53
|
});
|
|
56
54
|
|
|
57
55
|
this.global.define('print', arg => { console.log(arg); return null; });
|
|
58
56
|
this.global.define('type', arg => {
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
if (Array.isArray(arg)) return 'array';
|
|
58
|
+
return typeof arg;
|
|
61
59
|
});
|
|
62
60
|
this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
|
|
63
61
|
this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
|
|
64
62
|
|
|
65
|
-
this.global.define('ask', prompt =>
|
|
66
|
-
const readlineSync = require('readline-sync');
|
|
67
|
-
return readlineSync.question(prompt + ' ');
|
|
68
|
-
});
|
|
63
|
+
this.global.define('ask', prompt => readlineSync.question(prompt + ' '));
|
|
69
64
|
this.global.define('num', arg => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
this.global.define('str', arg => {
|
|
78
|
-
return String(arg);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
65
|
+
const n = Number(arg);
|
|
66
|
+
if (Number.isNaN(n)) throw new Error('Cannot convert value to number');
|
|
67
|
+
return n;
|
|
68
|
+
});
|
|
69
|
+
this.global.define('str', arg => String(arg));
|
|
70
|
+
}
|
|
83
71
|
|
|
84
72
|
evaluate(node, env = this.global) {
|
|
85
73
|
switch (node.type) {
|
|
@@ -96,6 +84,7 @@ this.global.define('str', arg => {
|
|
|
96
84
|
case 'LogicalExpression': return this.evalLogical(node, env);
|
|
97
85
|
case 'UnaryExpression': return this.evalUnary(node, env);
|
|
98
86
|
case 'Literal': return node.value;
|
|
87
|
+
case 'TemplateLiteral': return this.evalTemplateLiteral(node, env);
|
|
99
88
|
case 'Identifier': return env.get(node.name);
|
|
100
89
|
case 'IfStatement': return this.evalIf(node, env);
|
|
101
90
|
case 'WhileStatement': return this.evalWhile(node, env);
|
|
@@ -121,72 +110,65 @@ this.global.define('str', arg => {
|
|
|
121
110
|
|
|
122
111
|
evalProgram(node, env) {
|
|
123
112
|
let result = null;
|
|
124
|
-
for (const stmt of node.body)
|
|
125
|
-
result = this.evaluate(stmt, env);
|
|
126
|
-
}
|
|
113
|
+
for (const stmt of node.body) result = this.evaluate(stmt, env);
|
|
127
114
|
return result;
|
|
128
115
|
}
|
|
129
|
-
evalImport(node, env) {
|
|
130
|
-
const spec = node.path;
|
|
131
|
-
let lib;
|
|
132
116
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!fs.existsSync(fullPath)) {
|
|
144
|
-
throw new Error(`Import not found: ${spec}`);
|
|
117
|
+
evalTemplateLiteral(node, env) {
|
|
118
|
+
// node.parts is an array from parser: Literal or expression nodes
|
|
119
|
+
let result = '';
|
|
120
|
+
for (const part of node.parts) {
|
|
121
|
+
if (part.type === 'Literal') {
|
|
122
|
+
result += part.value;
|
|
123
|
+
} else {
|
|
124
|
+
const val = this.evaluate(part, env);
|
|
125
|
+
result += val != null ? val.toString() : '';
|
|
145
126
|
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
146
130
|
|
|
147
|
-
const code = fs.readFileSync(fullPath, 'utf-8');
|
|
148
|
-
const tokens = new Lexer(code).getTokens();
|
|
149
|
-
const ast = new Parser(tokens).parse();
|
|
150
|
-
|
|
151
|
-
const moduleEnv = new Environment(env);
|
|
152
|
-
this.evaluate(ast, moduleEnv);
|
|
153
131
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
132
|
+
evalImport(node, env) {
|
|
133
|
+
const spec = node.path;
|
|
134
|
+
let lib;
|
|
135
|
+
try {
|
|
136
|
+
const resolved = require.resolve(spec, { paths: [process.cwd()] });
|
|
137
|
+
lib = require(resolved);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
const fullPath = path.isAbsolute(spec)
|
|
140
|
+
? spec
|
|
141
|
+
: path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
|
|
142
|
+
|
|
143
|
+
if (!fs.existsSync(fullPath)) throw new Error(`Import not found: ${spec}`);
|
|
144
|
+
|
|
145
|
+
const code = fs.readFileSync(fullPath, 'utf-8');
|
|
146
|
+
const tokens = new Lexer(code).getTokens();
|
|
147
|
+
const ast = new Parser(tokens).parse();
|
|
148
|
+
const moduleEnv = new Environment(env);
|
|
149
|
+
this.evaluate(ast, moduleEnv);
|
|
150
|
+
|
|
151
|
+
lib = {};
|
|
152
|
+
for (const key of Object.keys(moduleEnv.store)) lib[key] = moduleEnv.store[key];
|
|
153
|
+
lib.default = lib;
|
|
157
154
|
}
|
|
158
155
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
if (imp.type === 'NamespaceImport') {
|
|
167
|
-
env.define(imp.local, lib);
|
|
168
|
-
}
|
|
169
|
-
if (imp.type === 'NamedImport') {
|
|
170
|
-
if (!(imp.imported in lib)) {
|
|
171
|
-
throw new Error(`Module '${spec}' has no export '${imp.imported}'`);
|
|
156
|
+
for (const imp of node.specifiers) {
|
|
157
|
+
if (imp.type === 'DefaultImport') env.define(imp.local, lib.default ?? lib);
|
|
158
|
+
if (imp.type === 'NamespaceImport') env.define(imp.local, lib);
|
|
159
|
+
if (imp.type === 'NamedImport') {
|
|
160
|
+
if (!(imp.imported in lib)) throw new Error(`Module '${spec}' has no export '${imp.imported}'`);
|
|
161
|
+
env.define(imp.local, lib[imp.imported]);
|
|
172
162
|
}
|
|
173
|
-
env.define(imp.local, lib[imp.imported]);
|
|
174
163
|
}
|
|
164
|
+
return null;
|
|
175
165
|
}
|
|
176
166
|
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
167
|
evalBlock(node, env) {
|
|
182
168
|
let result = null;
|
|
183
169
|
for (const stmt of node.body) {
|
|
184
|
-
try {
|
|
185
|
-
|
|
186
|
-
} catch (e) {
|
|
187
|
-
if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
|
|
188
|
-
throw e;
|
|
189
|
-
}
|
|
170
|
+
try { result = this.evaluate(stmt, env); }
|
|
171
|
+
catch (e) { if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e; else throw e; }
|
|
190
172
|
}
|
|
191
173
|
return result;
|
|
192
174
|
}
|
|
@@ -199,27 +181,15 @@ evalImport(node, env) {
|
|
|
199
181
|
evalAssignment(node, env) {
|
|
200
182
|
const rightVal = this.evaluate(node.right, env);
|
|
201
183
|
const left = node.left;
|
|
202
|
-
|
|
203
184
|
if (left.type === 'Identifier') return env.set(left.name, rightVal);
|
|
204
|
-
if (left.type === 'MemberExpression') {
|
|
205
|
-
|
|
206
|
-
obj[left.property] = rightVal;
|
|
207
|
-
return rightVal;
|
|
208
|
-
}
|
|
209
|
-
if (left.type === 'IndexExpression') {
|
|
210
|
-
const obj = this.evaluate(left.object, env);
|
|
211
|
-
const idx = this.evaluate(left.indexer, env);
|
|
212
|
-
obj[idx] = rightVal;
|
|
213
|
-
return rightVal;
|
|
214
|
-
}
|
|
215
|
-
|
|
185
|
+
if (left.type === 'MemberExpression') { const obj = this.evaluate(left.object, env); obj[left.property] = rightVal; return rightVal; }
|
|
186
|
+
if (left.type === 'IndexExpression') { const obj = this.evaluate(left.object, env); const idx = this.evaluate(left.indexer, env); obj[idx] = rightVal; return rightVal; }
|
|
216
187
|
throw new Error('Invalid assignment target');
|
|
217
188
|
}
|
|
218
189
|
|
|
219
190
|
evalCompoundAssignment(node, env) {
|
|
220
191
|
const left = node.left;
|
|
221
192
|
let current;
|
|
222
|
-
|
|
223
193
|
if (left.type === 'Identifier') current = env.get(left.name);
|
|
224
194
|
else if (left.type === 'MemberExpression') current = this.evalMember(left, env);
|
|
225
195
|
else if (left.type === 'IndexExpression') current = this.evalIndex(left, env);
|
|
@@ -237,9 +207,7 @@ evalImport(node, env) {
|
|
|
237
207
|
}
|
|
238
208
|
|
|
239
209
|
if (left.type === 'Identifier') env.set(left.name, computed);
|
|
240
|
-
else if (left.type === 'MemberExpression') this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
|
|
241
210
|
else this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
|
|
242
|
-
|
|
243
211
|
return computed;
|
|
244
212
|
}
|
|
245
213
|
|
|
@@ -251,8 +219,7 @@ evalImport(node, env) {
|
|
|
251
219
|
|
|
252
220
|
evalAsk(node, env) {
|
|
253
221
|
const prompt = this.evaluate(node.prompt, env);
|
|
254
|
-
|
|
255
|
-
return input;
|
|
222
|
+
return readlineSync.question(prompt + ' ');
|
|
256
223
|
}
|
|
257
224
|
|
|
258
225
|
evalDefine(node, env) {
|
|
@@ -269,12 +236,16 @@ evalImport(node, env) {
|
|
|
269
236
|
case 'STAR': return l * r;
|
|
270
237
|
case 'SLASH': return l / r;
|
|
271
238
|
case 'MOD': return l % r;
|
|
272
|
-
case 'EQEQ': return l
|
|
273
|
-
case 'NOTEQ': return l
|
|
239
|
+
case 'EQEQ': return l == r;
|
|
240
|
+
case 'NOTEQ': return l != r;
|
|
241
|
+
case 'STRICT_EQ': return l === r;
|
|
242
|
+
case 'STRICT_NOTEQ': return l !== r;
|
|
274
243
|
case 'LT': return l < r;
|
|
275
244
|
case 'LTE': return l <= r;
|
|
276
245
|
case 'GT': return l > r;
|
|
277
246
|
case 'GTE': return l >= r;
|
|
247
|
+
case 'LSHIFT': return l << r;
|
|
248
|
+
case 'RSHIFT': return l >> r;
|
|
278
249
|
default: throw new Error(`Unknown binary operator ${node.operator}`);
|
|
279
250
|
}
|
|
280
251
|
}
|
|
@@ -342,9 +313,7 @@ evalImport(node, env) {
|
|
|
342
313
|
const args = node.arguments.map(a => this.evaluate(a, env));
|
|
343
314
|
return calleeEvaluated(...args);
|
|
344
315
|
}
|
|
345
|
-
if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body)
|
|
346
|
-
throw new Error('Call to non-function');
|
|
347
|
-
}
|
|
316
|
+
if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) throw new Error('Call to non-function');
|
|
348
317
|
const fn = calleeEvaluated;
|
|
349
318
|
const callEnv = new Environment(fn.env);
|
|
350
319
|
fn.params.forEach((p, i) => {
|
|
@@ -384,15 +353,8 @@ evalImport(node, env) {
|
|
|
384
353
|
};
|
|
385
354
|
const setValue = (v) => {
|
|
386
355
|
if (arg.type === 'Identifier') env.set(arg.name, v);
|
|
387
|
-
else if (arg.type === 'MemberExpression') {
|
|
388
|
-
|
|
389
|
-
obj[arg.property] = v;
|
|
390
|
-
}
|
|
391
|
-
else if (arg.type === 'IndexExpression') {
|
|
392
|
-
const obj = this.evaluate(arg.object, env);
|
|
393
|
-
const idx = this.evaluate(arg.indexer, env);
|
|
394
|
-
obj[idx] = v;
|
|
395
|
-
}
|
|
356
|
+
else if (arg.type === 'MemberExpression') { const obj = this.evaluate(arg.object, env); obj[arg.property] = v; }
|
|
357
|
+
else if (arg.type === 'IndexExpression') { const obj = this.evaluate(arg.object, env); const idx = this.evaluate(arg.indexer, env); obj[idx] = v; }
|
|
396
358
|
};
|
|
397
359
|
const current = getCurrent();
|
|
398
360
|
const newVal = (node.operator === 'PLUSPLUS') ? current + 1 : current - 1;
|
|
@@ -401,4 +363,4 @@ evalImport(node, env) {
|
|
|
401
363
|
}
|
|
402
364
|
}
|
|
403
365
|
|
|
404
|
-
module.exports = Evaluator;
|
|
366
|
+
module.exports = Evaluator;
|
package/src/lexer.js
CHANGED
|
@@ -10,8 +10,8 @@ class Lexer {
|
|
|
10
10
|
this.currentChar = this.pos < this.input.length ? this.input[this.pos] : null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
peek() {
|
|
14
|
-
return this.pos +
|
|
13
|
+
peek(n = 1) {
|
|
14
|
+
return this.pos + n < this.input.length ? this.input[this.pos + n] : null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
error(msg) {
|
|
@@ -53,10 +53,21 @@ class Lexer {
|
|
|
53
53
|
result += this.currentChar;
|
|
54
54
|
this.advance();
|
|
55
55
|
}
|
|
56
|
-
return { type: 'NUMBER', value: parseFloat(result) };
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
if (this.currentChar && /[eE]/.test(this.currentChar)) {
|
|
59
|
+
result += this.currentChar; this.advance();
|
|
60
|
+
if (this.currentChar === '+' || this.currentChar === '-') {
|
|
61
|
+
result += this.currentChar; this.advance();
|
|
62
|
+
}
|
|
63
|
+
if (!/[0-9]/.test(this.currentChar)) this.error("Invalid exponent in number");
|
|
64
|
+
while (this.currentChar && /[0-9]/.test(this.currentChar)) {
|
|
65
|
+
result += this.currentChar;
|
|
66
|
+
this.advance();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { type: 'NUMBER', value: parseFloat(result) };
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
identifier() {
|
|
@@ -69,7 +80,7 @@ class Lexer {
|
|
|
69
80
|
const keywords = [
|
|
70
81
|
'let', 'sldeploy', 'if', 'else', 'while', 'for',
|
|
71
82
|
'break', 'continue', 'func', 'return', 'true', 'false', 'null',
|
|
72
|
-
'ask', 'define', 'import', 'from', 'as'
|
|
83
|
+
'ask', 'define', 'import', 'from', 'as', 'undefined'
|
|
73
84
|
];
|
|
74
85
|
|
|
75
86
|
if (keywords.includes(result)) {
|
|
@@ -77,8 +88,7 @@ class Lexer {
|
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
return { type: 'IDENTIFIER', value: result };
|
|
80
|
-
}
|
|
81
|
-
|
|
91
|
+
}
|
|
82
92
|
|
|
83
93
|
string() {
|
|
84
94
|
const quote = this.currentChar;
|
|
@@ -110,6 +120,46 @@ class Lexer {
|
|
|
110
120
|
return { type: 'STRING', value: result };
|
|
111
121
|
}
|
|
112
122
|
|
|
123
|
+
templateString() {
|
|
124
|
+
this.advance(); // skip initial backtick `
|
|
125
|
+
let buffer = '';
|
|
126
|
+
const tokens = [];
|
|
127
|
+
|
|
128
|
+
while (this.currentChar && this.currentChar !== '`') {
|
|
129
|
+
if (this.currentChar === '$' && this.peek() === '{') {
|
|
130
|
+
if (buffer.length > 0) {
|
|
131
|
+
tokens.push({ type: 'TEMPLATE_STRING', value: buffer });
|
|
132
|
+
buffer = '';
|
|
133
|
+
}
|
|
134
|
+
tokens.push({ type: 'DOLLAR_LBRACE' }); // ${
|
|
135
|
+
this.advance(); this.advance();
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this.currentChar === '\\') {
|
|
140
|
+
this.advance();
|
|
141
|
+
switch (this.currentChar) {
|
|
142
|
+
case 'n': buffer += '\n'; break;
|
|
143
|
+
case 't': buffer += '\t'; break;
|
|
144
|
+
case '`': buffer += '`'; break;
|
|
145
|
+
case '\\': buffer += '\\'; break;
|
|
146
|
+
default: buffer += this.currentChar;
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
buffer += this.currentChar;
|
|
150
|
+
}
|
|
151
|
+
this.advance();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (buffer.length > 0) tokens.push({ type: 'TEMPLATE_STRING', value: buffer });
|
|
155
|
+
|
|
156
|
+
if (this.currentChar !== '`') this.error('Unterminated template string');
|
|
157
|
+
this.advance(); // skip closing backtick
|
|
158
|
+
|
|
159
|
+
return tokens; // return array of tokens
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
113
163
|
getTokens() {
|
|
114
164
|
const tokens = [];
|
|
115
165
|
|
|
@@ -119,15 +169,26 @@ class Lexer {
|
|
|
119
169
|
if (/[0-9]/.test(this.currentChar)) { tokens.push(this.number()); continue; }
|
|
120
170
|
if (/[A-Za-z_]/.test(this.currentChar)) { tokens.push(this.identifier()); continue; }
|
|
121
171
|
if (this.currentChar === '"' || this.currentChar === "'") { tokens.push(this.string()); continue; }
|
|
172
|
+
if (this.currentChar === '`') {
|
|
173
|
+
const tTokens = this.templateString();
|
|
174
|
+
tokens.push(...tTokens); // push all tokens returned from templateString
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
122
178
|
|
|
123
179
|
const char = this.currentChar;
|
|
124
180
|
const next = this.peek();
|
|
181
|
+
const next2 = this.peek(2);
|
|
125
182
|
|
|
183
|
+
if (char === '=' && next === '=' && next2 === '=') { tokens.push({ type: 'STRICT_EQ' }); this.advance(); this.advance(); this.advance(); continue; }
|
|
184
|
+
if (char === '!' && next === '=' && next2 === '=') { tokens.push({ type: 'STRICT_NOTEQ' }); this.advance(); this.advance(); this.advance(); continue; }
|
|
126
185
|
if (char === '=' && next === '=') { tokens.push({ type: 'EQEQ' }); this.advance(); this.advance(); continue; }
|
|
127
186
|
if (char === '=' && next === '>') { tokens.push({ type: 'ARROW' }); this.advance(); this.advance(); continue; }
|
|
128
187
|
if (char === '!' && next === '=') { tokens.push({ type: 'NOTEQ' }); this.advance(); this.advance(); continue; }
|
|
129
188
|
if (char === '<' && next === '=') { tokens.push({ type: 'LTE' }); this.advance(); this.advance(); continue; }
|
|
130
189
|
if (char === '>' && next === '=') { tokens.push({ type: 'GTE' }); this.advance(); this.advance(); continue; }
|
|
190
|
+
if (char === '<' && next === '<') { tokens.push({ type: 'LSHIFT' }); this.advance(); this.advance(); continue; }
|
|
191
|
+
if (char === '>' && next === '>') { tokens.push({ type: 'RSHIFT' }); this.advance(); this.advance(); continue; }
|
|
131
192
|
if (char === '&' && next === '&') { tokens.push({ type: 'AND' }); this.advance(); this.advance(); continue; }
|
|
132
193
|
if (char === '|' && next === '|') { tokens.push({ type: 'OR' }); this.advance(); this.advance(); continue; }
|
|
133
194
|
if (char === '+' && next === '+') { tokens.push({ type: 'PLUSPLUS' }); this.advance(); this.advance(); continue; }
|
|
@@ -154,4 +215,4 @@ class Lexer {
|
|
|
154
215
|
}
|
|
155
216
|
}
|
|
156
217
|
|
|
157
|
-
module.exports = Lexer;
|
|
218
|
+
module.exports = Lexer;
|
package/src/parser.js
CHANGED
|
@@ -99,40 +99,41 @@ class Parser {
|
|
|
99
99
|
const body = this.block();
|
|
100
100
|
return { type: 'WhileStatement', test, body };
|
|
101
101
|
}
|
|
102
|
+
|
|
102
103
|
importStatement() {
|
|
103
104
|
this.eat('IMPORT');
|
|
104
105
|
|
|
105
106
|
let specifiers = [];
|
|
106
107
|
|
|
107
108
|
if (this.current.type === 'STAR') {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
this.eat('STAR');
|
|
110
|
+
this.eat('AS');
|
|
111
|
+
const name = this.current.value;
|
|
112
|
+
this.eat('IDENTIFIER');
|
|
113
|
+
specifiers.push({ type: 'NamespaceImport', local: name });
|
|
113
114
|
}
|
|
114
115
|
else if (this.current.type === 'LBRACE') {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
specifiers.push({ type: 'NamedImport', imported: importedName, local: localName });
|
|
126
|
-
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
116
|
+
this.eat('LBRACE');
|
|
117
|
+
while (this.current.type !== 'RBRACE') {
|
|
118
|
+
const importedName = this.current.value;
|
|
119
|
+
this.eat('IDENTIFIER');
|
|
120
|
+
let localName = importedName;
|
|
121
|
+
if (this.current.type === 'AS') {
|
|
122
|
+
this.eat('AS');
|
|
123
|
+
localName = this.current.value;
|
|
124
|
+
this.eat('IDENTIFIER');
|
|
127
125
|
}
|
|
128
|
-
|
|
126
|
+
specifiers.push({ type: 'NamedImport', imported: importedName, local: localName });
|
|
127
|
+
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
128
|
+
}
|
|
129
|
+
this.eat('RBRACE');
|
|
129
130
|
}
|
|
130
131
|
else if (this.current.type === 'IDENTIFIER') {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
const localName = this.current.value;
|
|
133
|
+
this.eat('IDENTIFIER');
|
|
134
|
+
specifiers.push({ type: 'DefaultImport', local: localName });
|
|
134
135
|
} else {
|
|
135
|
-
|
|
136
|
+
throw new Error('Unexpected token in import statement');
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
this.eat('FROM');
|
|
@@ -143,8 +144,7 @@ class Parser {
|
|
|
143
144
|
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
144
145
|
|
|
145
146
|
return { type: 'ImportStatement', path: pathToken.value, specifiers };
|
|
146
|
-
}
|
|
147
|
-
|
|
147
|
+
}
|
|
148
148
|
|
|
149
149
|
forStatement() {
|
|
150
150
|
this.eat('FOR');
|
|
@@ -224,6 +224,23 @@ class Parser {
|
|
|
224
224
|
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
225
225
|
return { type: 'ExpressionStatement', expression: expr };
|
|
226
226
|
}
|
|
227
|
+
parseTemplateLiteral() {
|
|
228
|
+
const parts = [];
|
|
229
|
+
while (true) {
|
|
230
|
+
if (this.current.type === 'TEMPLATE_STRING') {
|
|
231
|
+
parts.push({ type: 'Literal', value: this.current.value });
|
|
232
|
+
this.eat('TEMPLATE_STRING');
|
|
233
|
+
} else if (this.current.type === 'DOLLAR_LBRACE') {
|
|
234
|
+
this.eat('DOLLAR_LBRACE');
|
|
235
|
+
const expr = this.expression();
|
|
236
|
+
parts.push(expr);
|
|
237
|
+
this.eat('RBRACE');
|
|
238
|
+
} else {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { type: 'TemplateLiteral', parts };
|
|
243
|
+
}
|
|
227
244
|
|
|
228
245
|
expression() {
|
|
229
246
|
return this.assignment();
|
|
@@ -270,7 +287,7 @@ class Parser {
|
|
|
270
287
|
|
|
271
288
|
equality() {
|
|
272
289
|
let node = this.comparison();
|
|
273
|
-
while (['EQEQ', 'NOTEQ'].includes(this.current.type)) {
|
|
290
|
+
while (['EQEQ', 'NOTEQ', 'STRICT_EQ', 'STRICT_NOTEQ'].includes(this.current.type)) {
|
|
274
291
|
const op = this.current.type;
|
|
275
292
|
this.eat(op);
|
|
276
293
|
node = { type: 'BinaryExpression', operator: op, left: node, right: this.comparison() };
|
|
@@ -280,7 +297,7 @@ class Parser {
|
|
|
280
297
|
|
|
281
298
|
comparison() {
|
|
282
299
|
let node = this.term();
|
|
283
|
-
while (['LT', 'LTE', 'GT', 'GTE'].includes(this.current.type)) {
|
|
300
|
+
while (['LT', 'LTE', 'GT', 'GTE', 'LSHIFT', 'RSHIFT'].includes(this.current.type)) {
|
|
284
301
|
const op = this.current.type;
|
|
285
302
|
this.eat(op);
|
|
286
303
|
node = { type: 'BinaryExpression', operator: op, left: node, right: this.term() };
|
|
@@ -371,9 +388,14 @@ class Parser {
|
|
|
371
388
|
|
|
372
389
|
if (t.type === 'NUMBER') { this.eat('NUMBER'); return { type: 'Literal', value: t.value }; }
|
|
373
390
|
if (t.type === 'STRING') { this.eat('STRING'); return { type: 'Literal', value: t.value }; }
|
|
391
|
+
if (t.type === 'TEMPLATE_STRING' || t.type === 'DOLLAR_LBRACE') {
|
|
392
|
+
return this.parseTemplateLiteral();
|
|
393
|
+
}
|
|
394
|
+
|
|
374
395
|
if (t.type === 'TRUE') { this.eat('TRUE'); return { type: 'Literal', value: true }; }
|
|
375
396
|
if (t.type === 'FALSE') { this.eat('FALSE'); return { type: 'Literal', value: false }; }
|
|
376
397
|
if (t.type === 'NULL') { this.eat('NULL'); return { type: 'Literal', value: null }; }
|
|
398
|
+
if (t.type === 'UNDEFINED') { this.eat('UNDEFINED'); return { type: 'Literal', value: undefined }; }
|
|
377
399
|
|
|
378
400
|
if (t.type === 'ASK') {
|
|
379
401
|
this.eat('ASK');
|
|
@@ -438,4 +460,4 @@ class Parser {
|
|
|
438
460
|
}
|
|
439
461
|
}
|
|
440
462
|
|
|
441
|
-
module.exports = Parser;
|
|
463
|
+
module.exports = Parser;
|