tova 0.2.8 → 0.3.0
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 +49 -18
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +134 -62
- package/src/codegen/base-codegen.js +15 -5
- package/src/codegen/client-codegen.js +17 -4
- package/src/codegen/server-codegen.js +7 -2
- package/src/lexer/lexer.js +2 -1
- package/src/lsp/server.js +43 -23
- package/src/parser/parser.js +47 -19
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +49 -33
- package/src/stdlib/inline.js +7 -7
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -79,10 +79,10 @@ async function main() {
|
|
|
79
79
|
await runFile(args.filter(a => a !== '--strict')[1], { strict: isStrict });
|
|
80
80
|
break;
|
|
81
81
|
case 'build':
|
|
82
|
-
buildProject(args.slice(1));
|
|
82
|
+
await buildProject(args.slice(1));
|
|
83
83
|
break;
|
|
84
84
|
case 'dev':
|
|
85
|
-
devServer(args.slice(1));
|
|
85
|
+
await devServer(args.slice(1));
|
|
86
86
|
break;
|
|
87
87
|
case 'repl':
|
|
88
88
|
await startRepl();
|
|
@@ -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) {
|
|
@@ -1300,7 +1302,7 @@ async function migrateUp(args) {
|
|
|
1300
1302
|
await db.exec(sql);
|
|
1301
1303
|
}
|
|
1302
1304
|
const ph = db.driver === 'postgres' ? '$1' : '?';
|
|
1303
|
-
await db.
|
|
1305
|
+
await db.query(`INSERT INTO __migrations (name) VALUES (${ph})`, file);
|
|
1304
1306
|
console.log(` ✓ ${file}`);
|
|
1305
1307
|
}
|
|
1306
1308
|
|
|
@@ -1477,6 +1479,8 @@ async function startRepl() {
|
|
|
1477
1479
|
}
|
|
1478
1480
|
|
|
1479
1481
|
if (trimmed === ':clear') {
|
|
1482
|
+
for (const key of Object.keys(context)) delete context[key];
|
|
1483
|
+
delete context.__mutable;
|
|
1480
1484
|
initFn.call(context);
|
|
1481
1485
|
console.log(' Context cleared.\n');
|
|
1482
1486
|
rl.prompt();
|
|
@@ -1512,6 +1516,36 @@ async function startRepl() {
|
|
|
1512
1516
|
const output = compileTova(input, '<repl>');
|
|
1513
1517
|
const code = output.shared || '';
|
|
1514
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
|
+
|
|
1515
1549
|
// Try wrapping last expression statement as a return for value display
|
|
1516
1550
|
const lines = code.trim().split('\n');
|
|
1517
1551
|
const lastLine = lines[lines.length - 1].trim();
|
|
@@ -1522,28 +1556,25 @@ async function startRepl() {
|
|
|
1522
1556
|
const allButLast = lines.slice(0, -1).join('\n');
|
|
1523
1557
|
// Strip trailing semicolon from last line for the return
|
|
1524
1558
|
const returnExpr = lastLine.endsWith(';') ? lastLine.slice(0, -1) : lastLine;
|
|
1525
|
-
|
|
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 : '');
|
|
1526
1567
|
}
|
|
1527
1568
|
try {
|
|
1528
|
-
|
|
1529
|
-
const declaredInCode = new Set();
|
|
1530
|
-
for (const m of evalCode.matchAll(/\bfunction\s+([a-zA-Z_]\w*)/g)) declaredInCode.add(m[1]);
|
|
1531
|
-
for (const m of evalCode.matchAll(/\bconst\s+([a-zA-Z_]\w*)/g)) declaredInCode.add(m[1]);
|
|
1532
|
-
const ctxKeys = Object.keys(context).filter(k => !declaredInCode.has(k));
|
|
1533
|
-
const destructure = ctxKeys.length > 0 ? `const {${ctxKeys.join(',')}} = __ctx;` : '';
|
|
1534
|
-
const fn = new Function('__ctx', `${destructure}\n${evalCode}`);
|
|
1569
|
+
const fn = new Function('__ctx', `${destructure}${evalCode}`);
|
|
1535
1570
|
const result = fn(context);
|
|
1536
1571
|
if (result !== undefined) {
|
|
1537
1572
|
console.log(' ', result);
|
|
1538
1573
|
}
|
|
1539
1574
|
} catch (e) {
|
|
1540
1575
|
// If return-wrapping fails, fall back to plain execution
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
for (const m of code.matchAll(/\bconst\s+([a-zA-Z_]\w*)/g)) declaredInCode.add(m[1]);
|
|
1544
|
-
const ctxKeys = Object.keys(context).filter(k => !declaredInCode.has(k));
|
|
1545
|
-
const destructure = ctxKeys.length > 0 ? `const {${ctxKeys.join(',')}} = __ctx;` : '';
|
|
1546
|
-
const fn = new Function('__ctx', `${destructure}\n${code}`);
|
|
1576
|
+
const fallbackCode = code + (allSave ? '\n' + allSave : '');
|
|
1577
|
+
const fn = new Function('__ctx', `${destructure}${fallbackCode}`);
|
|
1547
1578
|
fn(context);
|
|
1548
1579
|
}
|
|
1549
1580
|
}
|
package/package.json
CHANGED
package/src/analyzer/analyzer.js
CHANGED
|
@@ -298,7 +298,9 @@ export class Analyzer {
|
|
|
298
298
|
if (['+', '-', '*', '/', '%', '**'].includes(expr.operator)) {
|
|
299
299
|
const lt = this._inferType(expr.left);
|
|
300
300
|
const rt = this._inferType(expr.right);
|
|
301
|
+
if (!lt && !rt) return null;
|
|
301
302
|
if (lt === 'Float' || rt === 'Float') return 'Float';
|
|
303
|
+
if (lt === 'String' || rt === 'String') return 'String';
|
|
302
304
|
return 'Int';
|
|
303
305
|
}
|
|
304
306
|
if (['==', '!=', '<', '>', '<=', '>='].includes(expr.operator)) return 'Bool';
|
|
@@ -567,8 +569,16 @@ export class Analyzer {
|
|
|
567
569
|
return;
|
|
568
570
|
case 'ObjectLiteral':
|
|
569
571
|
for (const prop of node.properties) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
+
if (prop.spread) {
|
|
573
|
+
// Spread property: {...expr}
|
|
574
|
+
this.visitExpression(prop.argument);
|
|
575
|
+
} else if (prop.shorthand) {
|
|
576
|
+
// Shorthand: {name} — key IS the variable reference
|
|
577
|
+
this.visitExpression(prop.key);
|
|
578
|
+
} else {
|
|
579
|
+
// Non-shorthand: {key: value} — only visit value, key is a label
|
|
580
|
+
this.visitExpression(prop.value);
|
|
581
|
+
}
|
|
572
582
|
}
|
|
573
583
|
return;
|
|
574
584
|
case 'ListComprehension':
|
|
@@ -712,10 +722,10 @@ export class Analyzer {
|
|
|
712
722
|
} finally {
|
|
713
723
|
this.currentScope = prevScope;
|
|
714
724
|
}
|
|
715
|
-
// Promote shared
|
|
716
|
-
// so server/client blocks can reference them
|
|
725
|
+
// Promote shared types and functions to parent scope
|
|
726
|
+
// so server/client blocks can reference them (but not variables)
|
|
717
727
|
for (const [name, sym] of sharedScope.symbols) {
|
|
718
|
-
if (!prevScope.symbols.has(name)) {
|
|
728
|
+
if (!prevScope.symbols.has(name) && (sym.kind === 'type' || sym.kind === 'function')) {
|
|
719
729
|
prevScope.symbols.set(name, sym);
|
|
720
730
|
}
|
|
721
731
|
}
|
|
@@ -832,7 +842,12 @@ export class Analyzer {
|
|
|
832
842
|
// Push expected return type for return-statement checking
|
|
833
843
|
const expectedReturn = node.returnType ? this._typeAnnotationToString(node.returnType) : null;
|
|
834
844
|
this._functionReturnTypeStack.push(expectedReturn);
|
|
835
|
-
|
|
845
|
+
const prevAsyncDepth = this._asyncDepth;
|
|
846
|
+
if (node.isAsync) {
|
|
847
|
+
this._asyncDepth++;
|
|
848
|
+
} else {
|
|
849
|
+
this._asyncDepth = 0; // Non-async function resets async context
|
|
850
|
+
}
|
|
836
851
|
|
|
837
852
|
try {
|
|
838
853
|
for (const param of node.params) {
|
|
@@ -861,7 +876,7 @@ export class Analyzer {
|
|
|
861
876
|
}
|
|
862
877
|
}
|
|
863
878
|
} finally {
|
|
864
|
-
|
|
879
|
+
this._asyncDepth = prevAsyncDepth;
|
|
865
880
|
this._functionReturnTypeStack.pop();
|
|
866
881
|
this.currentScope = prevScope;
|
|
867
882
|
}
|
|
@@ -1211,10 +1226,13 @@ export class Analyzer {
|
|
|
1211
1226
|
this.error(e.message);
|
|
1212
1227
|
}
|
|
1213
1228
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1229
|
+
try {
|
|
1230
|
+
for (const child of node.body) {
|
|
1231
|
+
this.visitNode(child);
|
|
1232
|
+
}
|
|
1233
|
+
} finally {
|
|
1234
|
+
this.currentScope = prevScope;
|
|
1216
1235
|
}
|
|
1217
|
-
this.currentScope = prevScope;
|
|
1218
1236
|
}
|
|
1219
1237
|
|
|
1220
1238
|
visitStoreDeclaration(node) {
|
|
@@ -1231,10 +1249,13 @@ export class Analyzer {
|
|
|
1231
1249
|
|
|
1232
1250
|
const prevScope = this.currentScope;
|
|
1233
1251
|
this.currentScope = this.currentScope.child('block');
|
|
1234
|
-
|
|
1235
|
-
|
|
1252
|
+
try {
|
|
1253
|
+
for (const child of node.body) {
|
|
1254
|
+
this.visitNode(child);
|
|
1255
|
+
}
|
|
1256
|
+
} finally {
|
|
1257
|
+
this.currentScope = prevScope;
|
|
1236
1258
|
}
|
|
1237
|
-
this.currentScope = prevScope;
|
|
1238
1259
|
}
|
|
1239
1260
|
|
|
1240
1261
|
visitRouteDeclaration(node) {
|
|
@@ -1288,8 +1309,11 @@ export class Analyzer {
|
|
|
1288
1309
|
this.error(e.message);
|
|
1289
1310
|
}
|
|
1290
1311
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1312
|
+
try {
|
|
1313
|
+
this.visitNode(node.body);
|
|
1314
|
+
} finally {
|
|
1315
|
+
this.currentScope = prevScope;
|
|
1316
|
+
}
|
|
1293
1317
|
}
|
|
1294
1318
|
|
|
1295
1319
|
visitHealthCheckDeclaration(node) {
|
|
@@ -1324,8 +1348,11 @@ export class Analyzer {
|
|
|
1324
1348
|
this.error(e.message);
|
|
1325
1349
|
}
|
|
1326
1350
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1351
|
+
try {
|
|
1352
|
+
this.visitNode(node.body);
|
|
1353
|
+
} finally {
|
|
1354
|
+
this.currentScope = prevScope;
|
|
1355
|
+
}
|
|
1329
1356
|
}
|
|
1330
1357
|
|
|
1331
1358
|
visitWebSocketDeclaration(node) {
|
|
@@ -1345,8 +1372,11 @@ export class Analyzer {
|
|
|
1345
1372
|
this.error(e.message);
|
|
1346
1373
|
}
|
|
1347
1374
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1375
|
+
try {
|
|
1376
|
+
this.visitNode(handler.body);
|
|
1377
|
+
} finally {
|
|
1378
|
+
this.currentScope = prevScope;
|
|
1379
|
+
}
|
|
1350
1380
|
}
|
|
1351
1381
|
}
|
|
1352
1382
|
|
|
@@ -1418,8 +1448,11 @@ export class Analyzer {
|
|
|
1418
1448
|
this.error(e.message);
|
|
1419
1449
|
}
|
|
1420
1450
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1451
|
+
try {
|
|
1452
|
+
this.visitNode(node.body);
|
|
1453
|
+
} finally {
|
|
1454
|
+
this.currentScope = prevScope;
|
|
1455
|
+
}
|
|
1423
1456
|
}
|
|
1424
1457
|
|
|
1425
1458
|
visitSubscribeDeclaration(node) {
|
|
@@ -1437,8 +1470,11 @@ export class Analyzer {
|
|
|
1437
1470
|
this.error(e.message);
|
|
1438
1471
|
}
|
|
1439
1472
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1473
|
+
try {
|
|
1474
|
+
this.visitNode(node.body);
|
|
1475
|
+
} finally {
|
|
1476
|
+
this.currentScope = prevScope;
|
|
1477
|
+
}
|
|
1442
1478
|
}
|
|
1443
1479
|
|
|
1444
1480
|
visitEnvDeclaration(node) {
|
|
@@ -1480,8 +1516,11 @@ export class Analyzer {
|
|
|
1480
1516
|
this.error(e.message);
|
|
1481
1517
|
}
|
|
1482
1518
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1519
|
+
try {
|
|
1520
|
+
this.visitNode(node.body);
|
|
1521
|
+
} finally {
|
|
1522
|
+
this.currentScope = prevScope;
|
|
1523
|
+
}
|
|
1485
1524
|
}
|
|
1486
1525
|
|
|
1487
1526
|
visitUploadDeclaration(node) {
|
|
@@ -1555,8 +1594,11 @@ export class Analyzer {
|
|
|
1555
1594
|
this.error(e.message);
|
|
1556
1595
|
}
|
|
1557
1596
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1597
|
+
try {
|
|
1598
|
+
this.visitNode(node.body);
|
|
1599
|
+
} finally {
|
|
1600
|
+
this.currentScope = prevScope;
|
|
1601
|
+
}
|
|
1560
1602
|
}
|
|
1561
1603
|
|
|
1562
1604
|
visitCacheDeclaration(node) {
|
|
@@ -1579,10 +1621,13 @@ export class Analyzer {
|
|
|
1579
1621
|
for (const p of node.params) {
|
|
1580
1622
|
this.currentScope.define(p.name, { kind: 'param' });
|
|
1581
1623
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1624
|
+
try {
|
|
1625
|
+
for (const stmt of node.body.body || []) {
|
|
1626
|
+
this.visitNode(stmt);
|
|
1627
|
+
}
|
|
1628
|
+
} finally {
|
|
1629
|
+
this.currentScope = prevScope;
|
|
1584
1630
|
}
|
|
1585
|
-
this.currentScope = prevScope;
|
|
1586
1631
|
}
|
|
1587
1632
|
|
|
1588
1633
|
visitModelDeclaration(node) {
|
|
@@ -1600,10 +1645,13 @@ export class Analyzer {
|
|
|
1600
1645
|
visitTestBlock(node) {
|
|
1601
1646
|
const prevScope = this.currentScope;
|
|
1602
1647
|
this.currentScope = this.currentScope.child('block');
|
|
1603
|
-
|
|
1604
|
-
|
|
1648
|
+
try {
|
|
1649
|
+
for (const stmt of node.body) {
|
|
1650
|
+
this.visitNode(stmt);
|
|
1651
|
+
}
|
|
1652
|
+
} finally {
|
|
1653
|
+
this.currentScope = prevScope;
|
|
1605
1654
|
}
|
|
1606
|
-
this.currentScope = prevScope;
|
|
1607
1655
|
}
|
|
1608
1656
|
|
|
1609
1657
|
// ─── Expression visitors ──────────────────────────────────
|
|
@@ -1639,7 +1687,12 @@ export class Analyzer {
|
|
|
1639
1687
|
|
|
1640
1688
|
const expectedReturn = node.returnType ? this._typeAnnotationToString(node.returnType) : null;
|
|
1641
1689
|
this._functionReturnTypeStack.push(expectedReturn);
|
|
1642
|
-
|
|
1690
|
+
const prevAsyncDepth = this._asyncDepth;
|
|
1691
|
+
if (node.isAsync) {
|
|
1692
|
+
this._asyncDepth++;
|
|
1693
|
+
} else {
|
|
1694
|
+
this._asyncDepth = 0; // Non-async lambda resets async context
|
|
1695
|
+
}
|
|
1643
1696
|
|
|
1644
1697
|
try {
|
|
1645
1698
|
for (const param of node.params) {
|
|
@@ -1662,7 +1715,7 @@ export class Analyzer {
|
|
|
1662
1715
|
this.visitExpression(node.body);
|
|
1663
1716
|
}
|
|
1664
1717
|
} finally {
|
|
1665
|
-
|
|
1718
|
+
this._asyncDepth = prevAsyncDepth;
|
|
1666
1719
|
this._functionReturnTypeStack.pop();
|
|
1667
1720
|
this.currentScope = prevScope;
|
|
1668
1721
|
}
|
|
@@ -1764,31 +1817,35 @@ export class Analyzer {
|
|
|
1764
1817
|
}
|
|
1765
1818
|
}
|
|
1766
1819
|
|
|
1767
|
-
// Check user-defined types —
|
|
1768
|
-
//
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
_collectTypeVariants(node, allVariants, coveredVariants, matchLoc) {
|
|
1776
|
-
if (node.type === 'TypeDeclaration') {
|
|
1777
|
-
const typeVariants = node.variants.filter(v => v.type === 'TypeVariant').map(v => v.name);
|
|
1778
|
-
// If any of the match arms reference a variant from this type, check all
|
|
1779
|
-
const relevantVariants = typeVariants.filter(v => coveredVariants.has(v));
|
|
1780
|
-
if (relevantVariants.length > 0) {
|
|
1820
|
+
// Check user-defined types — find the single best-matching type whose variants
|
|
1821
|
+
// contain ALL covered variant names (avoids false positives with shared names)
|
|
1822
|
+
const candidates = [];
|
|
1823
|
+
this._collectTypeCandidates(this.ast.body, coveredVariants, candidates);
|
|
1824
|
+
// Only warn if exactly one type contains all covered variants
|
|
1825
|
+
if (candidates.length === 1) {
|
|
1826
|
+
const [typeName, typeVariants] = candidates[0];
|
|
1781
1827
|
for (const v of typeVariants) {
|
|
1782
1828
|
if (!coveredVariants.has(v)) {
|
|
1783
|
-
this.warn(`Non-exhaustive match: missing '${v}' variant from type '${
|
|
1829
|
+
this.warn(`Non-exhaustive match: missing '${v}' variant from type '${typeName}'`, node.loc);
|
|
1784
1830
|
}
|
|
1785
1831
|
}
|
|
1786
1832
|
}
|
|
1787
1833
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
_collectTypeCandidates(nodes, coveredVariants, candidates) {
|
|
1837
|
+
for (const node of nodes) {
|
|
1838
|
+
if (node.type === 'TypeDeclaration') {
|
|
1839
|
+
const typeVariants = node.variants.filter(v => v.type === 'TypeVariant').map(v => v.name);
|
|
1840
|
+
if (typeVariants.length === 0) continue;
|
|
1841
|
+
// All covered variants must be contained in this type's variants
|
|
1842
|
+
const allCovered = [...coveredVariants].every(v => typeVariants.includes(v));
|
|
1843
|
+
if (allCovered) {
|
|
1844
|
+
candidates.push([node.name, typeVariants]);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
if (node.type === 'SharedBlock' || node.type === 'ServerBlock' || node.type === 'ClientBlock') {
|
|
1848
|
+
this._collectTypeCandidates(node.body, coveredVariants, candidates);
|
|
1792
1849
|
}
|
|
1793
1850
|
}
|
|
1794
1851
|
}
|
|
@@ -1963,7 +2020,8 @@ export class Analyzer {
|
|
|
1963
2020
|
return true;
|
|
1964
2021
|
case 'BlockStatement':
|
|
1965
2022
|
if (node.body.length === 0) return false;
|
|
1966
|
-
|
|
2023
|
+
// Any statement that definitely returns makes the block definitely return
|
|
2024
|
+
return node.body.some(stmt => this._definitelyReturns(stmt));
|
|
1967
2025
|
case 'IfStatement':
|
|
1968
2026
|
if (!node.elseBody) return false;
|
|
1969
2027
|
const consequentReturns = this._definitelyReturns(node.consequent);
|
|
@@ -1971,8 +2029,9 @@ export class Analyzer {
|
|
|
1971
2029
|
const allAlternatesReturn = (node.alternates || []).every(alt => this._definitelyReturns(alt.body));
|
|
1972
2030
|
return consequentReturns && elseReturns && allAlternatesReturn;
|
|
1973
2031
|
case 'GuardStatement':
|
|
1974
|
-
// Guard
|
|
1975
|
-
return
|
|
2032
|
+
// Guard only handles the failure case — when condition is true, execution falls through
|
|
2033
|
+
// A guard alone never guarantees return on ALL paths
|
|
2034
|
+
return false;
|
|
1976
2035
|
case 'MatchExpression': {
|
|
1977
2036
|
const hasWildcard = node.arms.some(arm =>
|
|
1978
2037
|
arm.pattern.type === 'WildcardPattern' ||
|
|
@@ -1983,15 +2042,15 @@ export class Analyzer {
|
|
|
1983
2042
|
}
|
|
1984
2043
|
case 'TryCatchStatement': {
|
|
1985
2044
|
const tryReturns = node.tryBody.length > 0 &&
|
|
1986
|
-
|
|
2045
|
+
node.tryBody.some(s => this._definitelyReturns(s));
|
|
1987
2046
|
const catchReturns = !node.catchBody || (node.catchBody.length > 0 &&
|
|
1988
|
-
|
|
2047
|
+
node.catchBody.some(s => this._definitelyReturns(s)));
|
|
1989
2048
|
return tryReturns && catchReturns;
|
|
1990
2049
|
}
|
|
1991
2050
|
case 'ExpressionStatement':
|
|
1992
2051
|
return this._definitelyReturns(node.expression);
|
|
1993
2052
|
case 'CallExpression':
|
|
1994
|
-
return
|
|
2053
|
+
return false;
|
|
1995
2054
|
default:
|
|
1996
2055
|
return false;
|
|
1997
2056
|
}
|
|
@@ -2006,7 +2065,14 @@ export class Analyzer {
|
|
|
2006
2065
|
const hasSpread = node.arguments.some(a => a.type === 'SpreadExpression');
|
|
2007
2066
|
if (hasSpread) return;
|
|
2008
2067
|
|
|
2009
|
-
|
|
2068
|
+
// Named arguments are collapsed into a single object at codegen
|
|
2069
|
+
const hasNamedArgs = node.arguments.some(a => a.type === 'NamedArgument');
|
|
2070
|
+
if (hasNamedArgs) {
|
|
2071
|
+
const positionalCount = node.arguments.filter(a => a.type !== 'NamedArgument').length;
|
|
2072
|
+
var actualCount = positionalCount + 1; // named args become one object
|
|
2073
|
+
} else {
|
|
2074
|
+
var actualCount = node.arguments.length;
|
|
2075
|
+
}
|
|
2010
2076
|
const name = node.callee.name;
|
|
2011
2077
|
|
|
2012
2078
|
if (actualCount > fnSym._totalParamCount) {
|
|
@@ -2052,6 +2118,12 @@ export class Analyzer {
|
|
|
2052
2118
|
this.strictError(`Type mismatch: '++' expects String on right side, but got ${rightType}`, node.loc);
|
|
2053
2119
|
}
|
|
2054
2120
|
} else if (['-', '*', '/', '%', '**'].includes(op)) {
|
|
2121
|
+
// String literal * Int is valid (string repeat) — skip warning for that case
|
|
2122
|
+
if (op === '*') {
|
|
2123
|
+
const leftIsStr = node.left.type === 'StringLiteral' || node.left.type === 'TemplateLiteral';
|
|
2124
|
+
const rightIsStr = node.right.type === 'StringLiteral' || node.right.type === 'TemplateLiteral';
|
|
2125
|
+
if (leftIsStr || rightIsStr) return;
|
|
2126
|
+
}
|
|
2055
2127
|
// Arithmetic: both sides must be numeric
|
|
2056
2128
|
const numerics = new Set(['Int', 'Float']);
|
|
2057
2129
|
if (leftType && !numerics.has(leftType) && leftType !== 'Any') {
|
|
@@ -2144,7 +2216,7 @@ export class Analyzer {
|
|
|
2144
2216
|
name: m.name,
|
|
2145
2217
|
paramTypes: (m.params || []).map(p => typeAnnotationToType(p.typeAnnotation)),
|
|
2146
2218
|
returnType: typeAnnotationToType(m.returnType),
|
|
2147
|
-
paramCount: (m.params || []).length,
|
|
2219
|
+
paramCount: (m.params || []).filter(p => p.name !== 'self').length,
|
|
2148
2220
|
}));
|
|
2149
2221
|
this.currentScope.define(node.name, sym);
|
|
2150
2222
|
|
|
@@ -679,11 +679,15 @@ export class BaseCodegen {
|
|
|
679
679
|
const right = this.genExpression(node.right);
|
|
680
680
|
const op = node.operator;
|
|
681
681
|
|
|
682
|
-
// String multiply: "ha" * 3 => "ha".repeat(3)
|
|
682
|
+
// String multiply: "ha" * 3 => "ha".repeat(3), also x * 3 when x is string
|
|
683
683
|
if (op === '*' &&
|
|
684
684
|
(node.left.type === 'StringLiteral' || node.left.type === 'TemplateLiteral')) {
|
|
685
685
|
return `${left}.repeat(${right})`;
|
|
686
686
|
}
|
|
687
|
+
if (op === '*' &&
|
|
688
|
+
(node.right.type === 'StringLiteral' || node.right.type === 'TemplateLiteral')) {
|
|
689
|
+
return `${right}.repeat(${left})`;
|
|
690
|
+
}
|
|
687
691
|
|
|
688
692
|
// Tova ?? is NaN-safe: catches null, undefined, AND NaN
|
|
689
693
|
if (op === '??') {
|
|
@@ -1027,7 +1031,7 @@ export class BaseCodegen {
|
|
|
1027
1031
|
if (node.type === 'TemplateLiteral') {
|
|
1028
1032
|
// Template literal with column references
|
|
1029
1033
|
const parts = node.parts.map(p => {
|
|
1030
|
-
if (p.type === 'text') return p.value;
|
|
1034
|
+
if (p.type === 'text') return p.value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
1031
1035
|
return `\${${this._genColumnBody(p.value)}}`;
|
|
1032
1036
|
});
|
|
1033
1037
|
return '`' + parts.join('') + '`';
|
|
@@ -1462,6 +1466,9 @@ export class BaseCodegen {
|
|
|
1462
1466
|
|
|
1463
1467
|
genObjectLiteral(node) {
|
|
1464
1468
|
const props = node.properties.map(p => {
|
|
1469
|
+
if (p.spread) {
|
|
1470
|
+
return `...${this.genExpression(p.argument)}`;
|
|
1471
|
+
}
|
|
1465
1472
|
if (p.shorthand) {
|
|
1466
1473
|
return this.genExpression(p.key);
|
|
1467
1474
|
}
|
|
@@ -1590,7 +1597,7 @@ export class BaseCodegen {
|
|
|
1590
1597
|
const fieldNames = node.variants.map(f => f.name);
|
|
1591
1598
|
const params = fieldNames.join(', ');
|
|
1592
1599
|
const obj = fieldNames.map(f => `${f}`).join(', ');
|
|
1593
|
-
lines.push(`${this.i()}${exportPrefix}function ${node.name}(${params}) { return { ${obj} }; }`);
|
|
1600
|
+
lines.push(`${this.i()}${exportPrefix}function ${node.name}(${params}) { return Object.assign(Object.create(${node.name}.prototype), { ${obj} }); }`);
|
|
1594
1601
|
}
|
|
1595
1602
|
|
|
1596
1603
|
// Derive clause: generate methods
|
|
@@ -1636,21 +1643,24 @@ export class BaseCodegen {
|
|
|
1636
1643
|
genImplDeclaration(node) {
|
|
1637
1644
|
const lines = [];
|
|
1638
1645
|
for (const method of node.methods) {
|
|
1646
|
+
const hasSelf = method.params.some(p => p.name === 'self');
|
|
1639
1647
|
const params = method.params.filter(p => p.name !== 'self');
|
|
1640
1648
|
const paramStr = this.genParams(params);
|
|
1641
1649
|
const hasPropagate = this._containsPropagate(method.body);
|
|
1642
1650
|
const asyncPrefix = method.isAsync ? 'async ' : '';
|
|
1643
1651
|
this.pushScope();
|
|
1652
|
+
if (hasSelf) this.declareVar('self');
|
|
1644
1653
|
for (const p of params) {
|
|
1645
1654
|
if (p.destructure) this._declareDestructureVars(p.destructure);
|
|
1646
1655
|
else this.declareVar(p.name);
|
|
1647
1656
|
}
|
|
1648
1657
|
const body = this.genBlockBody(method.body);
|
|
1649
1658
|
this.popScope();
|
|
1659
|
+
const selfBinding = hasSelf ? `\n${this.i()} const self = this;` : '';
|
|
1650
1660
|
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()}};`);
|
|
1661
|
+
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
1662
|
} else {
|
|
1653
|
-
lines.push(`${this.i()}${node.typeName}.prototype.${method.name} = ${asyncPrefix}function(${paramStr}) {\n${body}\n${this.i()}};`);
|
|
1663
|
+
lines.push(`${this.i()}${node.typeName}.prototype.${method.name} = ${asyncPrefix}function(${paramStr}) {${selfBinding}\n${body}\n${this.i()}};`);
|
|
1654
1664
|
}
|
|
1655
1665
|
}
|
|
1656
1666
|
return lines.join('\n');
|
|
@@ -27,6 +27,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
27
27
|
}
|
|
28
28
|
if (node.type === 'IfExpression') {
|
|
29
29
|
return this._containsRPC(node.condition) || this._containsRPC(node.consequent) ||
|
|
30
|
+
(node.alternates && node.alternates.some(a => this._containsRPC(a.condition) || this._containsRPC(a.body))) ||
|
|
30
31
|
this._containsRPC(node.elseBody);
|
|
31
32
|
}
|
|
32
33
|
if (node.type === 'ForStatement') return this._containsRPC(node.iterable) || this._containsRPC(node.body);
|
|
@@ -45,14 +46,15 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
45
46
|
return this._containsRPC(node.subject) || node.arms.some(a => this._containsRPC(a.body));
|
|
46
47
|
}
|
|
47
48
|
if (node.type === 'TryCatchStatement') {
|
|
48
|
-
return
|
|
49
|
-
this._containsRPC(
|
|
49
|
+
return (node.tryBody && node.tryBody.some(s => this._containsRPC(s))) ||
|
|
50
|
+
(node.catchBody && node.catchBody.some(s => this._containsRPC(s))) ||
|
|
51
|
+
(node.finallyBody && node.finallyBody.some(s => this._containsRPC(s)));
|
|
50
52
|
}
|
|
51
53
|
if (node.type === 'PipeExpression') {
|
|
52
54
|
return this._containsRPC(node.left) || this._containsRPC(node.right);
|
|
53
55
|
}
|
|
54
56
|
if (node.type === 'GuardStatement') {
|
|
55
|
-
return this._containsRPC(node.condition) || this._containsRPC(node.
|
|
57
|
+
return this._containsRPC(node.condition) || this._containsRPC(node.elseBody);
|
|
56
58
|
}
|
|
57
59
|
if (node.type === 'LetDestructure') return this._containsRPC(node.value);
|
|
58
60
|
if (node.type === 'ArrayLiteral') return node.elements.some(e => this._containsRPC(e));
|
|
@@ -61,7 +63,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
61
63
|
if (node.type === 'AwaitExpression') return this._containsRPC(node.argument);
|
|
62
64
|
if (node.type === 'PropagateExpression') return this._containsRPC(node.expression);
|
|
63
65
|
if (node.type === 'UnaryExpression') return this._containsRPC(node.operand);
|
|
64
|
-
if (node.type === 'TemplateLiteral') return node.parts.some(p => p.type === 'expr' && this._containsRPC(p.
|
|
66
|
+
if (node.type === 'TemplateLiteral') return node.parts.some(p => p.type === 'expr' && this._containsRPC(p.value));
|
|
65
67
|
if (node.type === 'ChainedComparison') return node.operands.some(o => this._containsRPC(o));
|
|
66
68
|
if (node.type === 'RangeExpression') return this._containsRPC(node.start) || this._containsRPC(node.end);
|
|
67
69
|
if (node.type === 'SliceExpression') return this._containsRPC(node.object) || this._containsRPC(node.start) || this._containsRPC(node.end) || this._containsRPC(node.step);
|
|
@@ -602,6 +604,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
602
604
|
if (node.type === 'ObjectLiteral') return node.properties.some(p => this._exprReadsSignal(p.value));
|
|
603
605
|
if (node.type === 'IfExpression') {
|
|
604
606
|
return this._exprReadsSignal(node.condition) || this._exprReadsSignal(node.consequent) ||
|
|
607
|
+
(node.alternates && node.alternates.some(a => this._exprReadsSignal(a.condition) || this._exprReadsSignal(a.body))) ||
|
|
605
608
|
this._exprReadsSignal(node.elseBody);
|
|
606
609
|
}
|
|
607
610
|
if (node.type === 'MatchExpression') {
|
|
@@ -876,6 +879,16 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
876
879
|
return `() => ${result}`;
|
|
877
880
|
}
|
|
878
881
|
|
|
882
|
+
// Override to add await for piped RPC calls
|
|
883
|
+
genPipeExpression(node) {
|
|
884
|
+
const result = super.genPipeExpression(node);
|
|
885
|
+
// If the pipe target is an RPC call and we're in async context, wrap with await
|
|
886
|
+
if (this._asyncContext && this._containsRPC(node.right)) {
|
|
887
|
+
return `await ${result}`;
|
|
888
|
+
}
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
|
|
879
892
|
// Override function declaration to make async if it contains server.* calls
|
|
880
893
|
genFunctionDeclaration(node) {
|
|
881
894
|
const hasRPC = this._containsRPC(node.body);
|