tova 0.8.2 → 0.9.4

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.
@@ -12,7 +12,23 @@ export function installBrowserAnalyzer(AnalyzerClass) {
12
12
 
13
13
  AnalyzerClass.prototype.visitBrowserBlock = function(node) {
14
14
  const prevScope = this.currentScope;
15
- this.currentScope = this.currentScope.child('browser');
15
+ let browserScope = null;
16
+ for (const ch of this.currentScope.children) {
17
+ if (ch.context === 'browser') { browserScope = ch; break; }
18
+ }
19
+ const isFirst = !browserScope;
20
+ this.currentScope = browserScope || this.currentScope.child('browser');
21
+
22
+ // On first browser block, pre-register state/computed/function names from ALL
23
+ // browser blocks so cross-file references resolve regardless of file order.
24
+ if (isFirst && this.ast && this.ast.body) {
25
+ for (const topNode of this.ast.body) {
26
+ if (topNode.type === 'BrowserBlock') {
27
+ this._preRegisterBrowserDecls(topNode.body);
28
+ }
29
+ }
30
+ }
31
+
16
32
  try {
17
33
  for (const stmt of node.body) {
18
34
  this.visitNode(stmt);
@@ -22,9 +38,26 @@ export function installBrowserAnalyzer(AnalyzerClass) {
22
38
  }
23
39
  };
24
40
 
41
+ AnalyzerClass.prototype._preRegisterBrowserDecls = function(stmts) {
42
+ for (const stmt of stmts) {
43
+ const name = stmt.name;
44
+ if (!name) continue;
45
+ let kind = null;
46
+ if (stmt.type === 'StateDeclaration') kind = 'state';
47
+ else if (stmt.type === 'ComputedDeclaration') kind = 'computed';
48
+ else if (stmt.type === 'FunctionDeclaration') kind = 'function';
49
+ else if (stmt.type === 'ComponentDeclaration') kind = 'component';
50
+ if (kind && !this.currentScope.symbols.has(name)) {
51
+ const sym = new Symbol(name, kind, stmt.typeAnnotation || null, kind === 'state', stmt.loc);
52
+ sym._forward = true;
53
+ try { this.currentScope.define(name, sym); } catch (e) { /* ignore */ }
54
+ }
55
+ }
56
+ };
57
+
25
58
  AnalyzerClass.prototype.visitStateDeclaration = function(node) {
26
59
  const ctx = this.currentScope.getContext();
27
- if (ctx !== 'browser') {
60
+ if (ctx !== 'browser' && !this._inPubComponent) {
28
61
  this.error(`'state' can only be used inside a browser block`, node.loc, "move this inside a browser { } block", { code: 'E302' });
29
62
  }
30
63
  try {
@@ -38,7 +71,7 @@ export function installBrowserAnalyzer(AnalyzerClass) {
38
71
 
39
72
  AnalyzerClass.prototype.visitComputedDeclaration = function(node) {
40
73
  const ctx = this.currentScope.getContext();
41
- if (ctx !== 'browser') {
74
+ if (ctx !== 'browser' && !this._inPubComponent) {
42
75
  this.error(`'computed' can only be used inside a browser block`, node.loc, "move this inside a browser { } block", { code: 'E302' });
43
76
  }
44
77
  try {
@@ -52,7 +85,7 @@ export function installBrowserAnalyzer(AnalyzerClass) {
52
85
 
53
86
  AnalyzerClass.prototype.visitEffectDeclaration = function(node) {
54
87
  const ctx = this.currentScope.getContext();
55
- if (ctx !== 'browser') {
88
+ if (ctx !== 'browser' && !this._inPubComponent) {
56
89
  this.error(`'effect' can only be used inside a browser block`, node.loc, "move this inside a browser { } block", { code: 'E302' });
57
90
  }
58
91
  this.visitNode(node.body);
@@ -60,19 +93,32 @@ export function installBrowserAnalyzer(AnalyzerClass) {
60
93
 
61
94
  AnalyzerClass.prototype.visitComponentDeclaration = function(node) {
62
95
  const ctx = this.currentScope.getContext();
63
- if (ctx !== 'browser') {
96
+ if (ctx !== 'browser' && !node.isPublic) {
64
97
  this.error(`'component' can only be used inside a browser block`, node.loc, "move this inside a browser { } block", { code: 'E302' });
65
98
  }
66
- this._checkNamingConvention(node.name, 'component', node.loc);
99
+ // Skip naming convention check for compound components (e.g. Dialog.Title)
100
+ // since "Dialog.Title" isn't a single PascalCase identifier
101
+ if (!node.parent) {
102
+ this._checkNamingConvention(node.name, 'component', node.loc);
103
+ }
104
+ // For compound components, register with the parent name (already defined)
105
+ // and use the full name for the symbol definition
106
+ const symbolName = node.name;
67
107
  try {
68
- this.currentScope.define(node.name,
69
- new Symbol(node.name, 'component', null, false, node.loc));
108
+ this.currentScope.define(symbolName,
109
+ new Symbol(symbolName, 'component', null, false, node.loc));
70
110
  } catch (e) {
71
111
  this.error(e.message);
72
112
  }
73
113
 
74
114
  const prevScope = this.currentScope;
75
115
  this.currentScope = this.currentScope.child('function');
116
+ // Store component prop names for variant() validation in style blocks
117
+ const prevComponentProps = this._currentComponentProps;
118
+ this._currentComponentProps = node.params.map(p => p.name);
119
+ // Track pub component context so state/computed/effect are allowed inside
120
+ const prevInPubComponent = this._inPubComponent;
121
+ if (node.isPublic) this._inPubComponent = true;
76
122
  for (const param of node.params) {
77
123
  try {
78
124
  this.currentScope.define(param.name,
@@ -86,6 +132,8 @@ export function installBrowserAnalyzer(AnalyzerClass) {
86
132
  this.visitNode(child);
87
133
  }
88
134
  } finally {
135
+ this._currentComponentProps = prevComponentProps;
136
+ this._inPubComponent = prevInPubComponent;
89
137
  this.currentScope = prevScope;
90
138
  }
91
139
  };
@@ -28,6 +28,13 @@ export class Scope {
28
28
  const existing = this.symbols.get(name);
29
29
  // Allow user code to shadow builtins
30
30
  if (existing.kind === 'builtin') {
31
+ if (existing.used) symbol.used = true;
32
+ this.symbols.set(name, symbol);
33
+ return;
34
+ }
35
+ // Allow real definitions to overwrite forward-declared symbols
36
+ if (existing._forward) {
37
+ if (existing.used) symbol.used = true;
31
38
  this.symbols.set(name, symbol);
32
39
  return;
33
40
  }
@@ -37,7 +37,22 @@ export function installServerAnalyzer(AnalyzerClass) {
37
37
  const prevScope = this.currentScope;
38
38
  const prevServerBlockName = this._currentServerBlockName;
39
39
  this._currentServerBlockName = node.name || null;
40
- this.currentScope = this.currentScope.child('server');
40
+ let serverScope = null;
41
+ for (const ch of this.currentScope.children) {
42
+ if (ch.context === 'server') { serverScope = ch; break; }
43
+ }
44
+ const isFirst = !serverScope;
45
+ this.currentScope = serverScope || this.currentScope.child('server');
46
+
47
+ // On first server block, pre-register function/var/type names from ALL
48
+ // server blocks so cross-file references resolve regardless of file order.
49
+ if (isFirst && this.ast && this.ast.body) {
50
+ for (const topNode of this.ast.body) {
51
+ if (topNode.type === 'ServerBlock') {
52
+ this._preRegisterServerDecls(topNode.body);
53
+ }
54
+ }
55
+ }
41
56
 
42
57
  try {
43
58
  // Register peer server block names as valid identifiers in this scope
@@ -76,6 +91,23 @@ export function installServerAnalyzer(AnalyzerClass) {
76
91
  }
77
92
  };
78
93
 
94
+ AnalyzerClass.prototype._preRegisterServerDecls = function(stmts) {
95
+ for (const stmt of stmts) {
96
+ const name = stmt.name;
97
+ if (!name) continue;
98
+ let kind = null;
99
+ if (stmt.type === 'FunctionDeclaration') kind = 'function';
100
+ else if (stmt.type === 'VarDeclaration') kind = 'variable';
101
+ else if (stmt.type === 'TypeDeclaration') kind = 'type';
102
+ else if (stmt.type === 'ModelDeclaration') kind = 'type';
103
+ if (kind && !this.currentScope.symbols.has(name)) {
104
+ const sym = new Symbol(name, kind, stmt.typeAnnotation || null, kind === 'variable', stmt.loc);
105
+ sym._forward = true;
106
+ try { this.currentScope.define(name, sym); } catch (e) { /* ignore */ }
107
+ }
108
+ }
109
+ };
110
+
79
111
  AnalyzerClass.prototype.visitRouteDeclaration = function(node) {
80
112
  const ctx = this.currentScope.getContext();
81
113
  if (ctx !== 'server') {
@@ -189,10 +189,25 @@ export class BaseCodegen {
189
189
  // Track a builtin and its transitive dependencies from the stdlib dependency graph
190
190
  _trackBuiltin(name) {
191
191
  this._usedBuiltins.add(name);
192
- const deps = STDLIB_DEPS[name];
193
- if (deps) {
194
- for (const dep of deps) {
195
- this._usedBuiltins.add(dep);
192
+ // Check if this name or any transitive dependency requires Result/Option
193
+ if (name === 'Ok' || name === 'Err' || name === 'Some' || name === 'None') {
194
+ this._needsResultOption = true;
195
+ }
196
+ // Resolve transitive deps using a queue (e.g., iter -> Seq -> Some/None)
197
+ const queue = [name];
198
+ while (queue.length > 0) {
199
+ const current = queue.pop();
200
+ const deps = STDLIB_DEPS[current];
201
+ if (deps) {
202
+ for (const dep of deps) {
203
+ if (!this._usedBuiltins.has(dep)) {
204
+ this._usedBuiltins.add(dep);
205
+ queue.push(dep); // resolve further transitive deps
206
+ }
207
+ if (dep === 'Ok' || dep === 'Err' || dep === 'Some' || dep === 'None') {
208
+ this._needsResultOption = true;
209
+ }
210
+ }
196
211
  }
197
212
  }
198
213
  }
@@ -316,9 +331,12 @@ export class BaseCodegen {
316
331
  return this._paramSubstitutions.get(node.name);
317
332
  }
318
333
  // Track builtin identifier usage (e.g., None used without call)
334
+ // Use _trackBuiltin to resolve transitive deps (including Result/Option)
319
335
  if (BUILTIN_NAMES.has(node.name)) {
320
- this._usedBuiltins.add(node.name);
336
+ this._trackBuiltin(node.name);
321
337
  }
338
+ // Ok/Err/Some/None are in RESULT_OPTION, not BUILTIN_FUNCTIONS,
339
+ // so they need a separate check for _needsResultOption
322
340
  if (node.name === 'Ok' || node.name === 'Err' || node.name === 'Some' || node.name === 'None') {
323
341
  this._needsResultOption = true;
324
342
  }
@@ -619,7 +637,14 @@ export class BaseCodegen {
619
637
  }).join(', ');
620
638
  }
621
639
 
640
+ _rewriteImportSource(node) {
641
+ if (node.source.startsWith('tova:')) {
642
+ node.source = './runtime/' + node.source.slice(5) + '.js';
643
+ }
644
+ }
645
+
622
646
  genImport(node) {
647
+ this._rewriteImportSource(node);
623
648
  for (const s of node.specifiers) this.declareVar(s.local);
624
649
  const specs = node.specifiers.map(s => {
625
650
  if (s.imported !== s.local) return `${s.imported} as ${s.local}`;
@@ -629,11 +654,13 @@ export class BaseCodegen {
629
654
  }
630
655
 
631
656
  genImportDefault(node) {
657
+ this._rewriteImportSource(node);
632
658
  this.declareVar(node.local);
633
659
  return `${this.i()}import ${node.local} from ${JSON.stringify(node.source)};`;
634
660
  }
635
661
 
636
662
  genImportWildcard(node) {
663
+ this._rewriteImportSource(node);
637
664
  this.declareVar(node.local);
638
665
  return `${this.i()}import * as ${node.local} from ${JSON.stringify(node.source)};`;
639
666
  }
@@ -1522,6 +1549,10 @@ export class BaseCodegen {
1522
1549
  if (node.callee.name === 'iter') {
1523
1550
  this._needsResultOption = true; // Seq.first()/find() return Option
1524
1551
  }
1552
+ // LRUCache.get() returns Option — needs Result/Option
1553
+ if (node.callee.name === 'LRUCache') {
1554
+ this._needsResultOption = true;
1555
+ }
1525
1556
 
1526
1557
  // Inline string/collection builtins to direct method calls
1527
1558
  const inlined = this._tryInlineBuiltin(node);
@@ -1534,19 +1565,24 @@ export class BaseCodegen {
1534
1565
  BUILTIN_NAMES.has(node.callee.object.name)) {
1535
1566
  const ns = node.callee.object.name;
1536
1567
  this._trackBuiltin(ns);
1537
- // Namespaces that depend on Ok/Err need Result/Option
1568
+ // Namespaces that depend on Ok/Err/Some/None need Result/Option
1538
1569
  const deps = STDLIB_DEPS[ns];
1539
- if (deps && (deps.includes('Ok') || deps.includes('Err'))) {
1570
+ if (deps && (deps.includes('Ok') || deps.includes('Err') || deps.includes('Some') || deps.includes('None'))) {
1540
1571
  this._needsResultOption = true;
1541
1572
  }
1542
1573
  }
1543
1574
 
1544
1575
  // Check for table operation calls with column expressions
1545
1576
  const hasColumnExprs = node.arguments.some(a => this._containsColumnExpr(a));
1546
- if (hasColumnExprs || (node.callee.type === 'Identifier' && ['agg', 'table_agg'].includes(node.callee.name))) {
1577
+ if (hasColumnExprs || (node.callee.type === 'Identifier' && ['agg', 'table_agg', 'window', 'table_window'].includes(node.callee.name))) {
1547
1578
  const tableArgs = this._genTableCallArgs(node);
1548
1579
  if (tableArgs) {
1549
- const callee = this.genExpression(node.callee);
1580
+ let callee = this.genExpression(node.callee);
1581
+ // Remap window → table_window to avoid browser global conflict
1582
+ if (node.callee.type === 'Identifier' && node.callee.name === 'window') {
1583
+ callee = 'table_window';
1584
+ this._usedBuiltins.add('table_window');
1585
+ }
1550
1586
  return `${callee}(${tableArgs.join(', ')})`;
1551
1587
  }
1552
1588
  }
@@ -1555,6 +1591,15 @@ export class BaseCodegen {
1555
1591
  const hasNamedArgs = node.arguments.some(a => a.type === 'NamedArgument');
1556
1592
 
1557
1593
  if (hasNamedArgs) {
1594
+ // Check if callee is a type/variant constructor — reorder named args to positional
1595
+ const calleeName = node.callee.type === 'Identifier' ? node.callee.name : null;
1596
+ const fieldOrder = calleeName ? this._variantFields[calleeName] : null;
1597
+
1598
+ if (fieldOrder) {
1599
+ const result = this._reorderNamedArgsToPositional(node.arguments, fieldOrder);
1600
+ return `${callee}(${result.join(', ')})`;
1601
+ }
1602
+
1558
1603
  const allNamed = node.arguments.every(a => a.type === 'NamedArgument');
1559
1604
  if (allNamed) {
1560
1605
  // All named args → single object argument
@@ -1970,13 +2015,18 @@ export class BaseCodegen {
1970
2015
  if (right.type === 'CallExpression') {
1971
2016
  // Check for table operations with column expressions
1972
2017
  const hasColumnExprs = right.arguments.some(a => this._containsColumnExpr(a));
1973
- if (hasColumnExprs || (right.callee.type === 'Identifier' && ['agg', 'table_agg'].includes(right.callee.name))) {
2018
+ if (hasColumnExprs || (right.callee.type === 'Identifier' && ['agg', 'table_agg', 'window', 'table_window'].includes(right.callee.name))) {
1974
2019
  const tableArgs = this._genTableCallArgs(right);
1975
2020
  if (tableArgs) {
1976
- const callee = this.genExpression(right.callee);
1977
- // Track builtin usage
2021
+ let callee = this.genExpression(right.callee);
2022
+ // Remap window → table_window to avoid browser global conflict
2023
+ if (right.callee.type === 'Identifier' && right.callee.name === 'window') {
2024
+ callee = 'table_window';
2025
+ this._usedBuiltins.add('table_window');
2026
+ }
2027
+ // Track builtin usage (with dependency resolution)
1978
2028
  if (right.callee.type === 'Identifier' && BUILTIN_NAMES.has(right.callee.name)) {
1979
- this._usedBuiltins.add(right.callee.name);
2029
+ this._trackBuiltin(right.callee.name);
1980
2030
  }
1981
2031
  return `${callee}(${[left, ...tableArgs].join(', ')})`;
1982
2032
  }
@@ -2128,6 +2178,33 @@ export class BaseCodegen {
2128
2178
  return this.genExpression(node);
2129
2179
  }
2130
2180
 
2181
+ // Reorder named args to positional order for type/variant constructors
2182
+ _reorderNamedArgsToPositional(args, fieldOrder) {
2183
+ const slots = new Array(fieldOrder.length);
2184
+ let positionalIndex = 0;
2185
+ // First pass: place positional args in order
2186
+ for (const arg of args) {
2187
+ if (arg.type !== 'NamedArgument') {
2188
+ slots[positionalIndex] = this.genExpression(arg);
2189
+ positionalIndex++;
2190
+ }
2191
+ }
2192
+ // Second pass: place named args in their field's slot
2193
+ for (const arg of args) {
2194
+ if (arg.type === 'NamedArgument') {
2195
+ const idx = fieldOrder.indexOf(arg.name);
2196
+ if (idx !== -1) {
2197
+ slots[idx] = this.genExpression(arg.value);
2198
+ } else {
2199
+ // Unknown field — emit as-is (analyzer will warn)
2200
+ slots[positionalIndex] = this.genExpression(arg.value);
2201
+ positionalIndex++;
2202
+ }
2203
+ }
2204
+ }
2205
+ return slots.filter(s => s !== undefined);
2206
+ }
2207
+
2131
2208
  // Override genCallExpression to handle table operations with column expressions
2132
2209
  _genTableCallArgs(node) {
2133
2210
  const calleeName = node.callee.type === 'Identifier' ? node.callee.name : null;
@@ -2226,6 +2303,52 @@ export class BaseCodegen {
2226
2303
  return parts;
2227
2304
  }
2228
2305
 
2306
+ // window() / table_window() — partition_by/order_by/desc are meta, rest are window fns
2307
+ if (calleeName === 'window' || calleeName === 'table_window') {
2308
+ const META_KEYS = new Set(['partition_by', 'order_by', 'desc']);
2309
+ const optParts = [];
2310
+ const winParts = [];
2311
+ for (const a of node.arguments) {
2312
+ if (a.type === 'NamedArgument') {
2313
+ if (META_KEYS.has(a.name)) {
2314
+ // Meta keys: partition_by → partition, order_by → order
2315
+ const key = a.name === 'partition_by' ? 'partition' : a.name === 'order_by' ? 'order' : a.name;
2316
+ if (a.name === 'desc') {
2317
+ optParts.push(`desc: ${this.genExpression(a.value)}`);
2318
+ } else if (this._containsColumnExpr(a.value)) {
2319
+ optParts.push(`${key}: (__row) => ${this._genColumnBody(a.value)}`);
2320
+ } else {
2321
+ optParts.push(`${key}: ${this.genExpression(a.value)}`);
2322
+ }
2323
+ } else {
2324
+ // Window function column
2325
+ const val = a.value;
2326
+ if (val.type === 'CallExpression' && val.callee.type === 'Identifier') {
2327
+ const fnName = val.callee.name;
2328
+ const winFn = `win_${fnName}`;
2329
+ const WIN_FNS = ['row_number', 'rank', 'dense_rank', 'percent_rank', 'ntile',
2330
+ 'lag', 'lead', 'first_value', 'last_value',
2331
+ 'running_sum', 'running_count', 'running_avg', 'running_min', 'running_max',
2332
+ 'moving_avg'];
2333
+ if (WIN_FNS.includes(fnName)) {
2334
+ this._usedBuiltins.add(winFn);
2335
+ const innerArgs = val.arguments.map(ia => {
2336
+ if (this._containsColumnExpr(ia)) {
2337
+ return `(__row) => ${this._genColumnBody(ia)}`;
2338
+ }
2339
+ return this.genExpression(ia);
2340
+ });
2341
+ winParts.push(`${a.name}: ${winFn}(${innerArgs.join(', ')})`);
2342
+ continue;
2343
+ }
2344
+ }
2345
+ winParts.push(`${a.name}: ${this.genExpression(a.value)}`);
2346
+ }
2347
+ }
2348
+ }
2349
+ return [`{ ${optParts.join(', ')} }`, `{ ${winParts.join(', ')} }`];
2350
+ }
2351
+
2229
2352
  // drop_nil/fill_nil — column expression compiles to string or lambda
2230
2353
  if (calleeName === 'drop_nil' || calleeName === 'fill_nil') {
2231
2354
  return node.arguments.map(a => {
@@ -3138,6 +3261,7 @@ export class BaseCodegen {
3138
3261
  } else {
3139
3262
  this.declareVar(node.name);
3140
3263
  const fieldNames = node.variants.map(f => f.name);
3264
+ this._variantFields[node.name] = fieldNames;
3141
3265
  const params = fieldNames.join(', ');
3142
3266
  const obj = fieldNames.map(f => `${f}`).join(', ');
3143
3267
  lines.push(`${this.i()}${exportPrefix}function ${node.name}(${params}) { return Object.assign(Object.create(${node.name}.prototype), { ${obj} }); }`);