tova 0.2.7 → 0.2.9
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/bin/tova.js +68 -16
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +9 -2
- package/src/codegen/base-codegen.js +9 -3
- package/src/lexer/lexer.js +1 -0
- package/src/parser/parser.js +33 -14
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -352,7 +352,9 @@ async function runFile(filePath, options = {}) {
|
|
|
352
352
|
// Execute the generated JavaScript (with stdlib)
|
|
353
353
|
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
354
354
|
const stdlib = getRunStdlib();
|
|
355
|
-
|
|
355
|
+
let code = stdlib + '\n' + (output.shared || '') + '\n' + (output.server || output.client || '');
|
|
356
|
+
// Strip 'export ' keywords — not valid inside AsyncFunction (used in tova build only)
|
|
357
|
+
code = code.replace(/^export /gm, '');
|
|
356
358
|
const fn = new AsyncFunction(code);
|
|
357
359
|
await fn();
|
|
358
360
|
} catch (err) {
|
|
@@ -764,6 +766,18 @@ async function devServer(args) {
|
|
|
764
766
|
processes.push({ child, label: 'server', port });
|
|
765
767
|
rebuildPortOffset++;
|
|
766
768
|
}
|
|
769
|
+
|
|
770
|
+
// Wait for server to be ready before triggering browser reload
|
|
771
|
+
if (processes.length > 0) {
|
|
772
|
+
const serverPort = processes[0].port;
|
|
773
|
+
for (let i = 0; i < 50; i++) {
|
|
774
|
+
try {
|
|
775
|
+
const res = await fetch(`http://localhost:${serverPort}/`);
|
|
776
|
+
if (res.ok || res.status === 404) break;
|
|
777
|
+
} catch {}
|
|
778
|
+
await new Promise(r => setTimeout(r, 100));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
767
781
|
console.log(' ✓ Rebuild complete');
|
|
768
782
|
notifyReload();
|
|
769
783
|
});
|
|
@@ -791,7 +805,16 @@ async function generateDevHTML(clientCode, srcDir, reloadPort = 0) {
|
|
|
791
805
|
(function() {
|
|
792
806
|
var es = new EventSource("http://localhost:${reloadPort}/__tova_reload");
|
|
793
807
|
es.onmessage = function(e) { if (e.data === "reload") window.location.reload(); };
|
|
794
|
-
es.onerror = function() {
|
|
808
|
+
es.onerror = function() {
|
|
809
|
+
es.close();
|
|
810
|
+
// Server is rebuilding — poll until it's back, then reload
|
|
811
|
+
var check = setInterval(function() {
|
|
812
|
+
fetch(window.location.href, { mode: "no-cors" }).then(function() {
|
|
813
|
+
clearInterval(check);
|
|
814
|
+
window.location.reload();
|
|
815
|
+
}).catch(function() {});
|
|
816
|
+
}, 500);
|
|
817
|
+
};
|
|
795
818
|
})();
|
|
796
819
|
</script>` : '';
|
|
797
820
|
|
|
@@ -1456,6 +1479,8 @@ async function startRepl() {
|
|
|
1456
1479
|
}
|
|
1457
1480
|
|
|
1458
1481
|
if (trimmed === ':clear') {
|
|
1482
|
+
for (const key of Object.keys(context)) delete context[key];
|
|
1483
|
+
delete context.__mutable;
|
|
1459
1484
|
initFn.call(context);
|
|
1460
1485
|
console.log(' Context cleared.\n');
|
|
1461
1486
|
rl.prompt();
|
|
@@ -1491,6 +1516,36 @@ async function startRepl() {
|
|
|
1491
1516
|
const output = compileTova(input, '<repl>');
|
|
1492
1517
|
const code = output.shared || '';
|
|
1493
1518
|
if (code.trim()) {
|
|
1519
|
+
// Extract function/const/let names from compiled code
|
|
1520
|
+
const declaredInCode = new Set();
|
|
1521
|
+
for (const m of code.matchAll(/\bfunction\s+([a-zA-Z_]\w*)/g)) declaredInCode.add(m[1]);
|
|
1522
|
+
for (const m of code.matchAll(/\bconst\s+([a-zA-Z_]\w*)/g)) declaredInCode.add(m[1]);
|
|
1523
|
+
for (const m of code.matchAll(/\blet\s+([a-zA-Z_]\w*)/g)) {
|
|
1524
|
+
declaredInCode.add(m[1]);
|
|
1525
|
+
// Track mutable variables for proper let destructuring
|
|
1526
|
+
if (!context.__mutable) context.__mutable = new Set();
|
|
1527
|
+
context.__mutable.add(m[1]);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Save declared variables back to context for persistence across inputs
|
|
1531
|
+
const saveNewDecls = declaredInCode.size > 0
|
|
1532
|
+
? [...declaredInCode].map(n => `if(typeof ${n}!=='undefined')__ctx.${n}=${n};`).join('\n')
|
|
1533
|
+
: '';
|
|
1534
|
+
// Also save mutable variables that may have been modified (not newly declared)
|
|
1535
|
+
const mutKeys = context.__mutable
|
|
1536
|
+
? [...context.__mutable].filter(n => !declaredInCode.has(n) && n in context)
|
|
1537
|
+
: [];
|
|
1538
|
+
const saveMut = mutKeys.map(n => `__ctx.${n}=${n};`).join('\n');
|
|
1539
|
+
const allSave = [saveNewDecls, saveMut].filter(Boolean).join('\n');
|
|
1540
|
+
|
|
1541
|
+
// Context destructuring: use let for mutable, const for immutable
|
|
1542
|
+
const ctxKeys = Object.keys(context).filter(k => !declaredInCode.has(k) && k !== '__mutable');
|
|
1543
|
+
const constKeys = ctxKeys.filter(k => !context.__mutable || !context.__mutable.has(k));
|
|
1544
|
+
const letKeys = ctxKeys.filter(k => context.__mutable && context.__mutable.has(k));
|
|
1545
|
+
const destructure =
|
|
1546
|
+
(constKeys.length > 0 ? `const {${constKeys.join(',')}} = __ctx;\n` : '') +
|
|
1547
|
+
(letKeys.length > 0 ? `let {${letKeys.join(',')}} = __ctx;\n` : '');
|
|
1548
|
+
|
|
1494
1549
|
// Try wrapping last expression statement as a return for value display
|
|
1495
1550
|
const lines = code.trim().split('\n');
|
|
1496
1551
|
const lastLine = lines[lines.length - 1].trim();
|
|
@@ -1501,28 +1556,25 @@ async function startRepl() {
|
|
|
1501
1556
|
const allButLast = lines.slice(0, -1).join('\n');
|
|
1502
1557
|
// Strip trailing semicolon from last line for the return
|
|
1503
1558
|
const returnExpr = lastLine.endsWith(';') ? lastLine.slice(0, -1) : lastLine;
|
|
1504
|
-
|
|
1559
|
+
// Use try/finally so save runs after return expression evaluates (captures updated mutable values)
|
|
1560
|
+
if (allSave) {
|
|
1561
|
+
evalCode = `try {\n${allButLast}\nreturn (${returnExpr});\n} finally {\n${allSave}\n}`;
|
|
1562
|
+
} else {
|
|
1563
|
+
evalCode = allButLast + (allButLast ? '\n' : '') + `return (${returnExpr});`;
|
|
1564
|
+
}
|
|
1565
|
+
} else {
|
|
1566
|
+
evalCode = code + (allSave ? '\n' + allSave : '');
|
|
1505
1567
|
}
|
|
1506
1568
|
try {
|
|
1507
|
-
|
|
1508
|
-
const declaredInCode = new Set();
|
|
1509
|
-
for (const m of evalCode.matchAll(/\bfunction\s+([a-zA-Z_]\w*)/g)) declaredInCode.add(m[1]);
|
|
1510
|
-
for (const m of evalCode.matchAll(/\bconst\s+([a-zA-Z_]\w*)/g)) declaredInCode.add(m[1]);
|
|
1511
|
-
const ctxKeys = Object.keys(context).filter(k => !declaredInCode.has(k));
|
|
1512
|
-
const destructure = ctxKeys.length > 0 ? `const {${ctxKeys.join(',')}} = __ctx;` : '';
|
|
1513
|
-
const fn = new Function('__ctx', `${destructure}\n${evalCode}`);
|
|
1569
|
+
const fn = new Function('__ctx', `${destructure}${evalCode}`);
|
|
1514
1570
|
const result = fn(context);
|
|
1515
1571
|
if (result !== undefined) {
|
|
1516
1572
|
console.log(' ', result);
|
|
1517
1573
|
}
|
|
1518
1574
|
} catch (e) {
|
|
1519
1575
|
// If return-wrapping fails, fall back to plain execution
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
for (const m of code.matchAll(/\bconst\s+([a-zA-Z_]\w*)/g)) declaredInCode.add(m[1]);
|
|
1523
|
-
const ctxKeys = Object.keys(context).filter(k => !declaredInCode.has(k));
|
|
1524
|
-
const destructure = ctxKeys.length > 0 ? `const {${ctxKeys.join(',')}} = __ctx;` : '';
|
|
1525
|
-
const fn = new Function('__ctx', `${destructure}\n${code}`);
|
|
1576
|
+
const fallbackCode = code + (allSave ? '\n' + allSave : '');
|
|
1577
|
+
const fn = new Function('__ctx', `${destructure}${fallbackCode}`);
|
|
1526
1578
|
fn(context);
|
|
1527
1579
|
}
|
|
1528
1580
|
}
|
package/package.json
CHANGED
package/src/analyzer/analyzer.js
CHANGED
|
@@ -2006,7 +2006,14 @@ export class Analyzer {
|
|
|
2006
2006
|
const hasSpread = node.arguments.some(a => a.type === 'SpreadExpression');
|
|
2007
2007
|
if (hasSpread) return;
|
|
2008
2008
|
|
|
2009
|
-
|
|
2009
|
+
// Named arguments are collapsed into a single object at codegen
|
|
2010
|
+
const hasNamedArgs = node.arguments.some(a => a.type === 'NamedArgument');
|
|
2011
|
+
if (hasNamedArgs) {
|
|
2012
|
+
const positionalCount = node.arguments.filter(a => a.type !== 'NamedArgument').length;
|
|
2013
|
+
var actualCount = positionalCount + 1; // named args become one object
|
|
2014
|
+
} else {
|
|
2015
|
+
var actualCount = node.arguments.length;
|
|
2016
|
+
}
|
|
2010
2017
|
const name = node.callee.name;
|
|
2011
2018
|
|
|
2012
2019
|
if (actualCount > fnSym._totalParamCount) {
|
|
@@ -2144,7 +2151,7 @@ export class Analyzer {
|
|
|
2144
2151
|
name: m.name,
|
|
2145
2152
|
paramTypes: (m.params || []).map(p => typeAnnotationToType(p.typeAnnotation)),
|
|
2146
2153
|
returnType: typeAnnotationToType(m.returnType),
|
|
2147
|
-
paramCount: (m.params || []).length,
|
|
2154
|
+
paramCount: (m.params || []).filter(p => p.name !== 'self').length,
|
|
2148
2155
|
}));
|
|
2149
2156
|
this.currentScope.define(node.name, sym);
|
|
2150
2157
|
|
|
@@ -1462,6 +1462,9 @@ export class BaseCodegen {
|
|
|
1462
1462
|
|
|
1463
1463
|
genObjectLiteral(node) {
|
|
1464
1464
|
const props = node.properties.map(p => {
|
|
1465
|
+
if (p.spread) {
|
|
1466
|
+
return `...${this.genExpression(p.argument)}`;
|
|
1467
|
+
}
|
|
1465
1468
|
if (p.shorthand) {
|
|
1466
1469
|
return this.genExpression(p.key);
|
|
1467
1470
|
}
|
|
@@ -1590,7 +1593,7 @@ export class BaseCodegen {
|
|
|
1590
1593
|
const fieldNames = node.variants.map(f => f.name);
|
|
1591
1594
|
const params = fieldNames.join(', ');
|
|
1592
1595
|
const obj = fieldNames.map(f => `${f}`).join(', ');
|
|
1593
|
-
lines.push(`${this.i()}${exportPrefix}function ${node.name}(${params}) { return { ${obj} }; }`);
|
|
1596
|
+
lines.push(`${this.i()}${exportPrefix}function ${node.name}(${params}) { return Object.assign(Object.create(${node.name}.prototype), { ${obj} }); }`);
|
|
1594
1597
|
}
|
|
1595
1598
|
|
|
1596
1599
|
// Derive clause: generate methods
|
|
@@ -1636,21 +1639,24 @@ export class BaseCodegen {
|
|
|
1636
1639
|
genImplDeclaration(node) {
|
|
1637
1640
|
const lines = [];
|
|
1638
1641
|
for (const method of node.methods) {
|
|
1642
|
+
const hasSelf = method.params.some(p => p.name === 'self');
|
|
1639
1643
|
const params = method.params.filter(p => p.name !== 'self');
|
|
1640
1644
|
const paramStr = this.genParams(params);
|
|
1641
1645
|
const hasPropagate = this._containsPropagate(method.body);
|
|
1642
1646
|
const asyncPrefix = method.isAsync ? 'async ' : '';
|
|
1643
1647
|
this.pushScope();
|
|
1648
|
+
if (hasSelf) this.declareVar('self');
|
|
1644
1649
|
for (const p of params) {
|
|
1645
1650
|
if (p.destructure) this._declareDestructureVars(p.destructure);
|
|
1646
1651
|
else this.declareVar(p.name);
|
|
1647
1652
|
}
|
|
1648
1653
|
const body = this.genBlockBody(method.body);
|
|
1649
1654
|
this.popScope();
|
|
1655
|
+
const selfBinding = hasSelf ? `\n${this.i()} const self = this;` : '';
|
|
1650
1656
|
if (hasPropagate) {
|
|
1651
|
-
lines.push(`${this.i()}${node.typeName}.prototype.${method.name} = ${asyncPrefix}function(${paramStr}) {\n${this.i()} try {\n${body}\n${this.i()} } catch (__e) {\n${this.i()} if (__e && __e.__tova_propagate) return __e.value;\n${this.i()} throw __e;\n${this.i()} }\n${this.i()}};`);
|
|
1657
|
+
lines.push(`${this.i()}${node.typeName}.prototype.${method.name} = ${asyncPrefix}function(${paramStr}) {${selfBinding}\n${this.i()} try {\n${body}\n${this.i()} } catch (__e) {\n${this.i()} if (__e && __e.__tova_propagate) return __e.value;\n${this.i()} throw __e;\n${this.i()} }\n${this.i()}};`);
|
|
1652
1658
|
} else {
|
|
1653
|
-
lines.push(`${this.i()}${node.typeName}.prototype.${method.name} = ${asyncPrefix}function(${paramStr}) {\n${body}\n${this.i()}};`);
|
|
1659
|
+
lines.push(`${this.i()}${node.typeName}.prototype.${method.name} = ${asyncPrefix}function(${paramStr}) {${selfBinding}\n${body}\n${this.i()}};`);
|
|
1654
1660
|
}
|
|
1655
1661
|
}
|
|
1656
1662
|
return lines.join('\n');
|
package/src/lexer/lexer.js
CHANGED
package/src/parser/parser.js
CHANGED
|
@@ -5,7 +5,7 @@ export class Parser {
|
|
|
5
5
|
static MAX_EXPRESSION_DEPTH = 200;
|
|
6
6
|
|
|
7
7
|
constructor(tokens, filename = '<stdin>') {
|
|
8
|
-
this.tokens = tokens.filter(t => t.type !== TokenType.NEWLINE && t.type !== TokenType.DOCSTRING);
|
|
8
|
+
this.tokens = tokens.filter(t => t.type !== TokenType.NEWLINE && t.type !== TokenType.DOCSTRING && t.type !== TokenType.SEMICOLON);
|
|
9
9
|
this.rawTokens = tokens;
|
|
10
10
|
this.filename = filename;
|
|
11
11
|
this.pos = 0;
|
|
@@ -2846,6 +2846,25 @@ export class Parser {
|
|
|
2846
2846
|
return new AST.ArrayLiteral(elements, l);
|
|
2847
2847
|
}
|
|
2848
2848
|
|
|
2849
|
+
_parseObjectProperty() {
|
|
2850
|
+
// Spread property: ...expr
|
|
2851
|
+
if (this.check(TokenType.SPREAD)) {
|
|
2852
|
+
const sl = this.loc();
|
|
2853
|
+
this.advance();
|
|
2854
|
+
const argument = this.parseUnary();
|
|
2855
|
+
return { spread: true, argument };
|
|
2856
|
+
}
|
|
2857
|
+
const key = this.parseExpression();
|
|
2858
|
+
if (this.match(TokenType.COLON)) {
|
|
2859
|
+
const value = this.parseExpression();
|
|
2860
|
+
return { key, value, shorthand: false };
|
|
2861
|
+
}
|
|
2862
|
+
if (key.type === 'Identifier') {
|
|
2863
|
+
return { key, value: key, shorthand: true };
|
|
2864
|
+
}
|
|
2865
|
+
this.error("Expected ':' in object literal");
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2849
2868
|
parseObjectOrDictComprehension() {
|
|
2850
2869
|
const l = this.loc();
|
|
2851
2870
|
this.expect(TokenType.LBRACE);
|
|
@@ -2855,6 +2874,17 @@ export class Parser {
|
|
|
2855
2874
|
return new AST.ObjectLiteral([], l);
|
|
2856
2875
|
}
|
|
2857
2876
|
|
|
2877
|
+
// Check for spread as first element — always an object literal
|
|
2878
|
+
if (this.check(TokenType.SPREAD)) {
|
|
2879
|
+
const properties = [this._parseObjectProperty()];
|
|
2880
|
+
while (this.match(TokenType.COMMA)) {
|
|
2881
|
+
if (this.check(TokenType.RBRACE)) break;
|
|
2882
|
+
properties.push(this._parseObjectProperty());
|
|
2883
|
+
}
|
|
2884
|
+
this.expect(TokenType.RBRACE, "Expected '}'");
|
|
2885
|
+
return new AST.ObjectLiteral(properties, l);
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2858
2888
|
// Try to parse first key: value pair
|
|
2859
2889
|
const firstKey = this.parseExpression();
|
|
2860
2890
|
|
|
@@ -2883,10 +2913,7 @@ export class Parser {
|
|
|
2883
2913
|
const properties = [{ key: firstKey, value: firstValue, shorthand: false }];
|
|
2884
2914
|
while (this.match(TokenType.COMMA)) {
|
|
2885
2915
|
if (this.check(TokenType.RBRACE)) break;
|
|
2886
|
-
|
|
2887
|
-
this.expect(TokenType.COLON, "Expected ':' in object literal");
|
|
2888
|
-
const value = this.parseExpression();
|
|
2889
|
-
properties.push({ key, value, shorthand: false });
|
|
2916
|
+
properties.push(this._parseObjectProperty());
|
|
2890
2917
|
}
|
|
2891
2918
|
|
|
2892
2919
|
this.expect(TokenType.RBRACE, "Expected '}'");
|
|
@@ -2898,15 +2925,7 @@ export class Parser {
|
|
|
2898
2925
|
const properties = [{ key: firstKey, value: firstKey, shorthand: true }];
|
|
2899
2926
|
while (this.match(TokenType.COMMA)) {
|
|
2900
2927
|
if (this.check(TokenType.RBRACE)) break;
|
|
2901
|
-
|
|
2902
|
-
if (this.match(TokenType.COLON)) {
|
|
2903
|
-
// Colon property: y: 10
|
|
2904
|
-
const value = this.parseExpression();
|
|
2905
|
-
properties.push({ key, value, shorthand: false });
|
|
2906
|
-
} else {
|
|
2907
|
-
// Shorthand property: y
|
|
2908
|
-
properties.push({ key, value: key, shorthand: true });
|
|
2909
|
-
}
|
|
2928
|
+
properties.push(this._parseObjectProperty());
|
|
2910
2929
|
}
|
|
2911
2930
|
this.expect(TokenType.RBRACE, "Expected '}'");
|
|
2912
2931
|
return new AST.ObjectLiteral(properties, l);
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-runtime.js — do not edit
|
|
2
|
-
export const VERSION = "0.2.
|
|
2
|
+
export const VERSION = "0.2.9";
|