tova 0.11.22 → 0.11.26

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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.11.22",
3
+ "version": "0.11.26",
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",
7
7
  "bin": {
8
- "tova": "./bin/tova.js"
8
+ "tova": "bin/tova.js"
9
9
  },
10
10
  "files": [
11
11
  "bin/",
@@ -133,6 +133,7 @@ export class Analyzer {
133
133
  this._allScopes = []; // Track all scopes for unused variable checking
134
134
  this._functionReturnTypeStack = []; // Stack of expected return types for type checking
135
135
  this._asyncDepth = 0; // Track nesting inside async functions for await validation
136
+ this._hasDefaultExport = false; // Track duplicate default exports
136
137
 
137
138
  // Propagate strict mode to the type system
138
139
  Type.strictMode = this.strict;
@@ -779,6 +780,8 @@ export class Analyzer {
779
780
  case 'ImportDefault': return this.visitImportDefault(node);
780
781
  case 'ImportWildcard': return this.visitImportWildcard(node);
781
782
  case 'ReExportDeclaration': return; // re-exports don't define local symbols
783
+ case 'ExportDefault': return this.visitExportDefault(node);
784
+ case 'ExportList': return this.visitExportList(node);
782
785
  case 'IfStatement': return this.visitIfStatement(node);
783
786
  case 'ForStatement': return this.visitForStatement(node);
784
787
  case 'WhileStatement': return this.visitWhileStatement(node);
@@ -1689,6 +1692,44 @@ export class Analyzer {
1689
1692
  }
1690
1693
  }
1691
1694
 
1695
+ visitExportDefault(node) {
1696
+ // Module-level restriction: not valid inside server/browser/edge/shared blocks
1697
+ const ctx = this.currentScope.getContext();
1698
+ if (ctx === 'server' || ctx === 'client' || ctx === 'browser' || ctx === 'shared') {
1699
+ this.warn("'export default' is only valid at module level", node.loc, null, { code: 'W_EXPORT_NOT_MODULE_LEVEL' });
1700
+ }
1701
+
1702
+ // Track duplicate default exports
1703
+ if (this._hasDefaultExport) {
1704
+ this.warn('Module already has a default export', node.loc, null, { code: 'W_DUPLICATE_DEFAULT_EXPORT' });
1705
+ }
1706
+ this._hasDefaultExport = true;
1707
+
1708
+ // Visit the inner value
1709
+ if (node.value) {
1710
+ this.visitNode(node.value);
1711
+ }
1712
+ }
1713
+
1714
+ visitExportList(node) {
1715
+ // Module-level restriction: not valid inside server/browser/edge/shared blocks
1716
+ const ctx = this.currentScope.getContext();
1717
+ if (ctx === 'server' || ctx === 'client' || ctx === 'browser' || ctx === 'shared') {
1718
+ this.warn("'export { }' is only valid at module level", node.loc, null, { code: 'W_EXPORT_NOT_MODULE_LEVEL' });
1719
+ }
1720
+
1721
+ for (const spec of node.specifiers) {
1722
+ // Check that the referenced name exists in scope
1723
+ const sym = this.currentScope.lookup(spec.local);
1724
+ if (!sym) {
1725
+ this.warn(`'${spec.local}' is not defined`, spec.loc, null, { code: 'W201' });
1726
+ } else {
1727
+ // Mark as public so it doesn't trigger unused warnings
1728
+ sym.isPublic = true;
1729
+ }
1730
+ }
1731
+ }
1732
+
1692
1733
  // ─── Statement visitors ───────────────────────────────────
1693
1734
 
1694
1735
  visitBlock(node) {
@@ -396,6 +396,18 @@ export function collectExports(ast, filename) {
396
396
  // Wildcard re-exports: pub * from "module" — can't enumerate statically,
397
397
  // but mark as having re-exports so import validation can allow through
398
398
  }
399
+ if (node.type === 'ExportDefault') {
400
+ publicExports.add('default');
401
+ allNames.add('default');
402
+ // Also collect the inner value's name if it's a named declaration
403
+ if (node.value) collectFromNode(node.value);
404
+ }
405
+ if (node.type === 'ExportList') {
406
+ for (const spec of node.specifiers) {
407
+ publicExports.add(spec.exported);
408
+ allNames.add(spec.exported);
409
+ }
410
+ }
399
411
  }
400
412
 
401
413
  for (const node of ast.body) {
package/src/cli/repl.js CHANGED
@@ -416,10 +416,14 @@ async function startRepl() {
416
416
  }
417
417
  } catch (e) {
418
418
  // If return-wrapping fails, fall back to plain execution
419
- const fallbackCode = replCode + (allSave ? '\n' + allSave : '');
420
- // REPL context: fallback execution of compiled Tova code (intentional dynamic eval)
421
- const fn = new Function('__ctx', `${destructure}${fallbackCode}`);
422
- fn(context);
419
+ try {
420
+ const fallbackCode = replCode + (allSave ? '\n' + allSave : '');
421
+ // REPL context: fallback execution of compiled Tova code (intentional dynamic eval)
422
+ const fn = new Function('__ctx', `${destructure}${fallbackCode}`);
423
+ fn(context);
424
+ } catch (e2) {
425
+ console.error(` Error: ${e2.message}`);
426
+ }
423
427
  }
424
428
  }
425
429
  } catch (err) {
@@ -3,6 +3,9 @@ import { RESULT_OPTION, PROPAGATE, BUILTIN_NAMES, STDLIB_DEPS } from '../stdlib/
3
3
  import { PIPE_TARGET } from '../parser/ast.js';
4
4
  import { compileWasmFunction, compileWasmModule, generateWasmGlue, generateMultiWasmGlue, generateWasmBytesExport } from './wasm-codegen.js';
5
5
 
6
+ // Async stdlib functions that should be auto-awaited at the top level (script context)
7
+ const _ASYNC_STDLIB_FUNCS = new Set(['read', 'write', 'readParquet', 'readExcel']);
8
+
6
9
  export class BaseCodegen {
7
10
  constructor() {
8
11
  this.indent = 0;
@@ -34,6 +37,8 @@ export class BaseCodegen {
34
37
  this._typedArrayLocals = new Map(); // varName -> 'Float64Array' | 'Int32Array' | 'Uint8Array'
35
38
  // Track shadowed parameter names to prevent incorrect substitution in nested lambdas
36
39
  this._substitutionShadowed = new Set(); // names shadowed by lambda/function params
40
+ // Track function nesting depth for auto-await of async stdlib functions
41
+ this._functionDepth = 0; // 0 = top level (scripts run in AsyncFunction wrapper)
37
42
  }
38
43
 
39
44
  static TYPED_ARRAY_MAP = {
@@ -282,6 +287,8 @@ export class BaseCodegen {
282
287
  case 'ImportDefault': result = this.genImportDefault(node); break;
283
288
  case 'ImportWildcard': result = this.genImportWildcard(node); break;
284
289
  case 'ReExportDeclaration': result = this.genReExport(node); break;
290
+ case 'ExportDefault': result = this.genExportDefault(node); break;
291
+ case 'ExportList': result = this.genExportList(node); break;
285
292
  case 'IfStatement': result = this.genIfStatement(node); break;
286
293
  case 'ForStatement': result = this.genForStatement(node); break;
287
294
  case 'WhileStatement': result = this.genWhileStatement(node); break;
@@ -573,6 +580,7 @@ export class BaseCodegen {
573
580
  }
574
581
 
575
582
  this.pushScope();
583
+ this._functionDepth++;
576
584
  for (const p of node.params) {
577
585
  if (p.destructure) {
578
586
  this._declareDestructureVars(p.destructure);
@@ -581,6 +589,7 @@ export class BaseCodegen {
581
589
  }
582
590
  }
583
591
  const body = this.genBlockBody(node.body);
592
+ this._functionDepth--;
584
593
  this.popScope();
585
594
 
586
595
  // Restore shadowing state
@@ -700,6 +709,31 @@ export class BaseCodegen {
700
709
  return `${this.i()}export { ${specs} } from ${JSON.stringify(node.source)};`;
701
710
  }
702
711
 
712
+ genExportDefault(node) {
713
+ if (node.value && node.value.type === 'FunctionDeclaration') {
714
+ // Generate the function without isPublic (ExportDefault handles export)
715
+ const savedPublic = node.value.isPublic;
716
+ node.value.isPublic = false;
717
+ const fnCode = this.genFunctionDeclaration(node.value);
718
+ node.value.isPublic = savedPublic;
719
+ // Insert 'export default ' before the function declaration
720
+ // Must handle: function, async function, async function*
721
+ return fnCode.replace(/^(\s*)(async\s+)?(?=function)/, '$1export default $2');
722
+ }
723
+ // Expression: export default <expr>;
724
+ // Unwrap ExpressionStatement if present
725
+ const valueNode = node.value.type === 'ExpressionStatement' ? node.value.expression : node.value;
726
+ const expr = this.genExpression(valueNode);
727
+ return `${this.i()}export default ${expr};`;
728
+ }
729
+
730
+ genExportList(node) {
731
+ const specs = node.specifiers.map(s =>
732
+ s.local === s.exported ? s.local : `${s.local} as ${s.exported}`
733
+ ).join(', ');
734
+ return `${this.i()}export { ${specs} };`;
735
+ }
736
+
703
737
  genIfStatement(node) {
704
738
  const p = [];
705
739
  p.push(`${this.i()}if (${this.genExpression(node.condition)}) {\n`);
@@ -1669,6 +1703,10 @@ export class BaseCodegen {
1669
1703
  }
1670
1704
 
1671
1705
  const args = node.arguments.map(a => this.genExpression(a)).join(', ');
1706
+ // Auto-await known async stdlib functions at top level (scripts run in AsyncFunction wrapper)
1707
+ if (this._functionDepth === 0 && node.callee.type === 'Identifier' && _ASYNC_STDLIB_FUNCS.has(node.callee.name)) {
1708
+ return `(await ${callee}(${args}))`;
1709
+ }
1672
1710
  return `${callee}(${args})`;
1673
1711
  }
1674
1712
 
@@ -2507,8 +2545,10 @@ export class BaseCodegen {
2507
2545
 
2508
2546
  if (node.body.type === 'BlockStatement') {
2509
2547
  this.pushScope();
2548
+ this._functionDepth++;
2510
2549
  for (const p of node.params) { if (p.destructure) this._declareDestructureVars(p.destructure); else this.declareVar(p.name); }
2511
2550
  const body = this.genBlockBody(node.body);
2551
+ this._functionDepth--;
2512
2552
  this.popScope();
2513
2553
  // Restore shadowing state
2514
2554
  for (const name of shadowedNames) {
@@ -2532,10 +2572,12 @@ export class BaseCodegen {
2532
2572
  // Statement bodies (compound assignment, assignment in lambda)
2533
2573
  if (node.body.type === 'CompoundAssignment' || node.body.type === 'Assignment' || node.body.type === 'VarDeclaration') {
2534
2574
  this.pushScope();
2575
+ this._functionDepth++;
2535
2576
  for (const p of node.params) { if (p.destructure) this._declareDestructureVars(p.destructure); else this.declareVar(p.name); }
2536
2577
  this.indent++;
2537
2578
  const stmt = this.generateStatement(node.body);
2538
2579
  this.indent--;
2580
+ this._functionDepth--;
2539
2581
  this.popScope();
2540
2582
  // Restore shadowing state
2541
2583
  for (const name of shadowedNames) {
@@ -2545,12 +2587,14 @@ export class BaseCodegen {
2545
2587
  }
2546
2588
 
2547
2589
  // Expression body
2590
+ this._functionDepth++;
2548
2591
  let bodyCode;
2549
2592
  if (hasPropagate) {
2550
2593
  bodyCode = `${asyncPrefix}(${params}) => { try { return ${this.genExpression(node.body)}; } catch (__e) { if (__e && __e.__tova_propagate) return __e.value; throw __e; } }`;
2551
2594
  } else {
2552
2595
  bodyCode = `${asyncPrefix}(${params}) => ${this.genExpression(node.body)}`;
2553
2596
  }
2597
+ this._functionDepth--;
2554
2598
  // Restore shadowing state
2555
2599
  for (const name of shadowedNames) {
2556
2600
  this._substitutionShadowed.delete(name);
@@ -56,6 +56,7 @@ export const TokenType = {
56
56
 
57
57
  // Visibility
58
58
  PUB: 'PUB',
59
+ DEFAULT: 'DEFAULT',
59
60
 
60
61
  // Impl blocks / traits
61
62
  IMPL: 'IMPL',
@@ -224,6 +225,7 @@ export const Keywords = {
224
225
  'loop': TokenType.LOOP,
225
226
  'when': TokenType.WHEN,
226
227
  'extern': TokenType.EXTERN,
228
+ 'default': TokenType.DEFAULT,
227
229
  'is': TokenType.IS,
228
230
  'with': TokenType.WITH,
229
231
  'server': TokenType.SERVER,
package/src/lsp/server.js CHANGED
@@ -518,7 +518,7 @@ class TovaLanguageServer {
518
518
  const keywords = [
519
519
  'fn', 'if', 'elif', 'else', 'for', 'while', 'loop', 'when', 'in',
520
520
  'return', 'match', 'type', 'import', 'from', 'true', 'false',
521
- 'nil', 'server', 'browser', 'client', 'shared', 'pub', 'mut',
521
+ 'nil', 'server', 'browser', 'client', 'shared', 'pub', 'export', 'default', 'mut',
522
522
  'try', 'catch', 'finally', 'break', 'continue', 'async', 'await',
523
523
  'guard', 'interface', 'derive', 'route', 'model', 'db',
524
524
  ];
package/src/parser/ast.js CHANGED
@@ -235,6 +235,33 @@ export class ReExportSpecifier {
235
235
  }
236
236
  }
237
237
 
238
+ // export default <value>
239
+ export class ExportDefault {
240
+ constructor(value, loc) {
241
+ this.type = 'ExportDefault';
242
+ this.value = value; // FunctionDeclaration or expression node
243
+ this.loc = loc;
244
+ }
245
+ }
246
+
247
+ // export { a, b as c } (post-declaration, no source/from)
248
+ export class ExportList {
249
+ constructor(specifiers, loc) {
250
+ this.type = 'ExportList';
251
+ this.specifiers = specifiers; // [{local, exported}]
252
+ this.loc = loc;
253
+ }
254
+ }
255
+
256
+ export class ExportListSpecifier {
257
+ constructor(local, exported, loc) {
258
+ this.type = 'ExportListSpecifier';
259
+ this.local = local; // name in current scope
260
+ this.exported = exported; // exported name (same as local if no alias)
261
+ this.loc = loc;
262
+ }
263
+ }
264
+
238
265
  // ============================================================
239
266
  // Statements
240
267
  // ============================================================
@@ -109,7 +109,7 @@ export class Parser {
109
109
  tok.type === TokenType.BROWSER || tok.type === TokenType.SHARED ||
110
110
  tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
111
111
  tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
112
- tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
112
+ tok.type === TokenType.PUB || tok.type === TokenType.EXPORT || tok.type === TokenType.DEFER ||
113
113
  tok.type === TokenType.EXTERN ||
114
114
  tok.type === TokenType.VAR || tok.type === TokenType.ASYNC) {
115
115
  return;
@@ -160,7 +160,7 @@ export class Parser {
160
160
  tok.type === TokenType.BROWSER || tok.type === TokenType.SHARED ||
161
161
  tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
162
162
  tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
163
- tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
163
+ tok.type === TokenType.PUB || tok.type === TokenType.EXPORT || tok.type === TokenType.DEFER ||
164
164
  tok.type === TokenType.EXTERN || tok.type === TokenType.VAR || tok.type === TokenType.MUT ||
165
165
  tok.type === TokenType.STATE || tok.type === TokenType.ROUTE ||
166
166
  tok.type === TokenType.IDENTIFIER) {
@@ -549,6 +549,7 @@ export class Parser {
549
549
  parseStatement() {
550
550
  // pub modifier: pub fn, pub type, pub x = ...
551
551
  if (this.check(TokenType.PUB)) return this.parsePubDeclaration();
552
+ if (this.check(TokenType.EXPORT)) return this.parsePubDeclaration();
552
553
  if (this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FOR) {
553
554
  this.advance(); // consume async
554
555
  return this.parseForStatement(null, true);
@@ -595,17 +596,35 @@ export class Parser {
595
596
 
596
597
  parsePubDeclaration() {
597
598
  const l = this.loc();
598
- this.advance(); // consume 'pub'
599
- if (this.check(TokenType.PUB)) {
600
- this.error("Duplicate 'pub' modifier");
599
+ const keyword = this.current().type; // PUB or EXPORT
600
+ this.advance(); // consume 'pub' or 'export'
601
+ if (this.check(TokenType.PUB) || this.check(TokenType.EXPORT)) {
602
+ this.error("Duplicate visibility modifier");
601
603
  }
602
- // Re-export: pub { a, b } from "module" or pub * from "module"
604
+ // export default: only valid with 'export', not 'pub'
605
+ if (this.check(TokenType.DEFAULT)) {
606
+ if (keyword === TokenType.PUB) {
607
+ this.error("Use 'export default', not 'pub default'");
608
+ }
609
+ this.advance(); // consume 'default'
610
+ // export default type is invalid (types generate multiple statements)
611
+ if (this.check(TokenType.TYPE)) {
612
+ this.error("Cannot use 'export default' with type declarations. Use 'export type' instead");
613
+ }
614
+ const stmt = this.parseStatement();
615
+ return new AST.ExportDefault(stmt, l);
616
+ }
617
+ // Re-export: pub/export { a, b } from "module" or pub/export * from "module"
603
618
  if (this.check(TokenType.STAR) && this.peek(1).type === TokenType.FROM) {
604
619
  return this.parseReExport(l);
605
620
  }
606
621
  if (this.check(TokenType.LBRACE) && this._looksLikeReExport()) {
607
622
  return this.parseReExport(l);
608
623
  }
624
+ // Post-declaration export list: pub/export { a, b } (no 'from')
625
+ if (this.check(TokenType.LBRACE) && this._looksLikeExportList()) {
626
+ return this.parseExportList(l);
627
+ }
609
628
  // Handle pub component at top level (parseComponent is installed by browser-parser plugin)
610
629
  if (this.check(TokenType.COMPONENT) && typeof this.parseComponent === 'function') {
611
630
  const comp = this.parseComponent();
@@ -644,10 +663,55 @@ export class Parser {
644
663
  }
645
664
  }
646
665
 
666
+ // Check if pub/export { ... } is a post-declaration export list (no 'from' after })
667
+ _looksLikeExportList() {
668
+ let i = 1; // start after {
669
+ while (true) {
670
+ const tok = this.peek(i);
671
+ if (!tok || tok.type === TokenType.EOF) return false;
672
+ if (tok.type === TokenType.RBRACE) {
673
+ // After }, must NOT see FROM (that would be a re-export)
674
+ const after = this.peek(i + 1);
675
+ return !after || after.type !== TokenType.FROM;
676
+ }
677
+ if (tok.type !== TokenType.IDENTIFIER) return false;
678
+ i++;
679
+ const next = this.peek(i);
680
+ if (next && next.type === TokenType.AS) {
681
+ i++; // skip as
682
+ i++; // skip alias identifier
683
+ }
684
+ const afterId = this.peek(i);
685
+ if (!afterId) return false;
686
+ if (afterId.type === TokenType.COMMA) { i++; continue; }
687
+ if (afterId.type === TokenType.RBRACE) continue;
688
+ return false;
689
+ }
690
+ }
691
+
692
+ parseExportList(l) {
693
+ this.expect(TokenType.LBRACE);
694
+ const specifiers = [];
695
+ while (!this.check(TokenType.RBRACE)) {
696
+ const specL = this.loc();
697
+ const local = this.expect(TokenType.IDENTIFIER, "Expected export name").value;
698
+ let exported = local;
699
+ if (this.match(TokenType.AS)) {
700
+ exported = this.expect(TokenType.IDENTIFIER, "Expected alias name after 'as'").value;
701
+ }
702
+ specifiers.push(new AST.ExportListSpecifier(local, exported, specL));
703
+ if (!this.check(TokenType.RBRACE)) {
704
+ this.expect(TokenType.COMMA, "Expected ',' or '}' in export list");
705
+ }
706
+ }
707
+ this.expect(TokenType.RBRACE);
708
+ return new AST.ExportList(specifiers, l);
709
+ }
710
+
647
711
  parseReExport(l) {
648
712
  if (this.match(TokenType.STAR)) {
649
- // pub * from "module"
650
- this.expect(TokenType.FROM, "Expected 'from' after 'pub *'");
713
+ // pub/export * from "module"
714
+ this.expect(TokenType.FROM, "Expected 'from' after '*'");
651
715
  const source = this.expect(TokenType.STRING, "Expected module path string").value;
652
716
  return new AST.ReExportDeclaration(null, source, l);
653
717
  }
@@ -989,7 +1053,7 @@ export class Parser {
989
1053
  params.push(param);
990
1054
  } else {
991
1055
  let name;
992
- if (this._isContextualKeyword()) {
1056
+ if (this._isContextualKeyword() || this.check(TokenType.DEFAULT)) {
993
1057
  name = this.advance().value;
994
1058
  } else {
995
1059
  name = this.expect(TokenType.IDENTIFIER, "Expected parameter name").value;
@@ -74,6 +74,7 @@ export const BUILTIN_FUNCTIONS = {
74
74
  var _pretty = function(v) {
75
75
  if (v === null || v === undefined) return v;
76
76
  if (typeof v !== 'object') return v;
77
+ if (v && typeof v._format === 'function') return v._format();
77
78
  if (Array.isArray(v) && v.length > 0 && typeof v[0] === 'object' && v[0] !== null && !Array.isArray(v[0])) {
78
79
  var headers = Object.keys(v[0]);
79
80
  var rows = v.map(function(r) { return headers.map(function(h) { return String(r[h] != null ? r[h] : ''); }); });
@@ -1897,6 +1898,19 @@ export const STDLIB_DEPS = {
1897
1898
  lazy: ['LazyTable', 'Table'],
1898
1899
  collect: ['LazyTable'],
1899
1900
  LazyTable: ['Table', 'table_where', 'table_group_by'],
1901
+ // Table wrapper functions reference LazyTable + their table_* implementation
1902
+ where: ['LazyTable', 'table_where', 'Table'],
1903
+ select: ['LazyTable', 'table_select', 'Table'],
1904
+ derive: ['LazyTable', 'table_derive', 'Table'],
1905
+ sort_by: ['LazyTable', 'table_sort_by', 'Table'],
1906
+ limit: ['LazyTable', 'table_limit', 'Table'],
1907
+ drop_duplicates: ['LazyTable', 'table_drop_duplicates', 'Table'],
1908
+ rename: ['LazyTable', 'table_rename', 'Table'],
1909
+ agg: ['table_agg', 'Table'],
1910
+ // Table functions that reference Table constructor
1911
+ drop_nil: ['Table'],
1912
+ fill_nil: ['Table'],
1913
+ cast: ['Table'],
1900
1914
  // Seq uses Some/None
1901
1915
  Seq: ['Some', 'None'],
1902
1916
  // Channel uses Some/None
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.11.22";
2
+ export const VERSION = "0.11.26";