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.
- package/bin/tova.js +1128 -137
- package/package.json +14 -2
- package/src/analyzer/analyzer.js +405 -9
- package/src/analyzer/browser-analyzer.js +56 -8
- package/src/analyzer/scope.js +7 -0
- package/src/analyzer/server-analyzer.js +33 -1
- package/src/codegen/base-codegen.js +137 -13
- package/src/codegen/browser-codegen.js +725 -20
- package/src/codegen/codegen.js +67 -5
- package/src/codegen/server-codegen.js +54 -6
- package/src/codegen/theme-codegen.js +69 -0
- package/src/config/module-path.js +34 -2
- package/src/config/resolve.js +9 -0
- package/src/config/toml.js +13 -1
- package/src/deploy/provision.js +6 -2
- package/src/diagnostics/security-scorecard.js +111 -0
- package/src/lexer/lexer.js +18 -3
- package/src/parser/animate-ast.js +45 -0
- package/src/parser/ast.js +15 -0
- package/src/parser/browser-ast.js +19 -1
- package/src/parser/browser-parser.js +221 -4
- package/src/parser/parser.js +21 -2
- package/src/parser/theme-ast.js +29 -0
- package/src/parser/theme-parser.js +70 -0
- package/src/registry/plugins/theme-plugin.js +20 -0
- package/src/registry/register-all.js +2 -0
- package/src/runtime/charts.js +547 -0
- package/src/runtime/embedded.js +6 -2
- package/src/runtime/reactivity.js +60 -0
- package/src/runtime/router.js +703 -295
- package/src/runtime/table.js +606 -33
- package/src/stdlib/inline.js +330 -7
- package/src/stdlib/string.js +84 -2
- package/src/stdlib/validation.js +1 -1
- package/src/version.js +1 -1
|
@@ -12,7 +12,23 @@ export function installBrowserAnalyzer(AnalyzerClass) {
|
|
|
12
12
|
|
|
13
13
|
AnalyzerClass.prototype.visitBrowserBlock = function(node) {
|
|
14
14
|
const prevScope = this.currentScope;
|
|
15
|
-
|
|
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
|
-
|
|
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(
|
|
69
|
-
new Symbol(
|
|
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
|
};
|
package/src/analyzer/scope.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1977
|
-
//
|
|
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.
|
|
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} }); }`);
|