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 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
- const code = stdlib + '\n' + (output.shared || '') + '\n' + (output.server || output.client || '');
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.exec(`INSERT INTO __migrations (name) VALUES ('${file.replace(/'/g, "''")}')`);
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
- evalCode = allButLast + (allButLast ? '\n' : '') + `return (${returnExpr});`;
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
- // Extract function/const names from compiled code to avoid shadowing conflicts
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 declaredInCode = new Set();
1542
- for (const m of code.matchAll(/\bfunction\s+([a-zA-Z_]\w*)/g)) declaredInCode.add(m[1]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "Tova — a modern programming language that transpiles to JavaScript, unifying frontend and backend",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
- this.visitExpression(prop.key);
571
- this.visitExpression(prop.value);
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 symbols (types, functions) to parent scope
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
- if (node.isAsync) this._asyncDepth++;
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
- if (node.isAsync) this._asyncDepth--;
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
- for (const child of node.body) {
1215
- this.visitNode(child);
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
- for (const child of node.body) {
1235
- this.visitNode(child);
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
- this.visitNode(node.body);
1292
- this.currentScope = prevScope;
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
- this.visitNode(node.body);
1328
- this.currentScope = prevScope;
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
- this.visitNode(handler.body);
1349
- this.currentScope = prevScope;
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
- this.visitNode(node.body);
1422
- this.currentScope = prevScope;
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
- this.visitNode(node.body);
1441
- this.currentScope = prevScope;
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
- this.visitNode(node.body);
1484
- this.currentScope = prevScope;
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
- this.visitNode(node.body);
1559
- this.currentScope = prevScope;
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
- for (const stmt of node.body.body || []) {
1583
- this.visitNode(stmt);
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
- for (const stmt of node.body) {
1604
- this.visitNode(stmt);
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
- if (node.isAsync) this._asyncDepth++;
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
- if (node.isAsync) this._asyncDepth--;
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 — look up in _variantFields from the global scope
1768
- // Collect all known type variants by iterating type declarations
1769
- for (const topNode of this.ast.body) {
1770
- this._collectTypeVariants(topNode, variantNames, coveredVariants, node.loc);
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 '${node.name}'`, matchLoc);
1829
+ this.warn(`Non-exhaustive match: missing '${v}' variant from type '${typeName}'`, node.loc);
1784
1830
  }
1785
1831
  }
1786
1832
  }
1787
1833
  }
1788
- // Recurse into blocks
1789
- if (node.type === 'SharedBlock' || node.type === 'ServerBlock' || node.type === 'ClientBlock') {
1790
- for (const child of node.body) {
1791
- this._collectTypeVariants(child, allVariants, coveredVariants, matchLoc);
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
- return this._definitelyReturns(node.body[node.body.length - 1]);
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's else block always runs if condition fails if it returns, the guard is a definite return path
1975
- return this._definitelyReturns(node.elseBody);
2032
+ // Guard only handles the failure casewhen 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
- this._definitelyReturns(node.tryBody[node.tryBody.length - 1]);
2045
+ node.tryBody.some(s => this._definitelyReturns(s));
1987
2046
  const catchReturns = !node.catchBody || (node.catchBody.length > 0 &&
1988
- this._definitelyReturns(node.catchBody[node.catchBody.length - 1]));
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 true;
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
- const actualCount = node.arguments.length;
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 this._containsRPC(node.tryBlock) || this._containsRPC(node.catchBlock) ||
49
- this._containsRPC(node.finallyBlock);
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.elseBlock);
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.expression));
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);