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 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
- 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) {
@@ -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() { setTimeout(function() { window.location.reload(); }, 1000); };
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
- 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 : '');
1505
1567
  }
1506
1568
  try {
1507
- // Extract function/const names from compiled code to avoid shadowing conflicts
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 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
- 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",
@@ -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
- const actualCount = node.arguments.length;
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');
@@ -435,6 +435,7 @@ export class Lexer {
435
435
  case '\\': current += '\\'; break;
436
436
  case '"': current += '"'; break;
437
437
  case '{': current += '{'; break;
438
+ case '}': current += '}'; break;
438
439
  default: current += '\\' + esc;
439
440
  }
440
441
  continue;
@@ -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
- const key = this.parseExpression();
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
- const key = this.parseExpression();
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.7";
2
+ export const VERSION = "0.2.9";