tova 0.1.1 → 0.2.2
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/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/tova.js +811 -154
- package/package.json +8 -2
- package/src/analyzer/analyzer.js +297 -58
- package/src/analyzer/scope.js +38 -1
- package/src/analyzer/type-registry.js +72 -0
- package/src/analyzer/types.js +478 -0
- package/src/codegen/base-codegen.js +371 -0
- package/src/codegen/client-codegen.js +62 -10
- package/src/codegen/codegen.js +111 -2
- package/src/codegen/server-codegen.js +175 -3
- package/src/config/edit-toml.js +100 -0
- package/src/config/package-json.js +52 -0
- package/src/config/resolve.js +100 -0
- package/src/config/toml.js +209 -0
- package/src/lexer/lexer.js +2 -2
- package/src/lsp/server.js +284 -30
- package/src/parser/ast.js +105 -0
- package/src/parser/parser.js +202 -2
- package/src/runtime/ai.js +305 -0
- package/src/runtime/devtools.js +228 -0
- package/src/runtime/embedded.js +3 -1
- package/src/runtime/io.js +240 -0
- package/src/runtime/reactivity.js +264 -19
- package/src/runtime/ssr.js +196 -24
- package/src/runtime/table.js +522 -0
- package/src/stdlib/collections.js +245 -0
- package/src/stdlib/core.js +87 -0
- package/src/stdlib/datetime.js +88 -0
- package/src/stdlib/encoding.js +35 -0
- package/src/stdlib/functional.js +82 -0
- package/src/stdlib/inline.js +334 -67
- package/src/stdlib/math.js +93 -0
- package/src/stdlib/string.js +95 -0
- package/src/stdlib/url.js +33 -0
- package/src/stdlib/validation.js +29 -0
|
@@ -157,6 +157,14 @@ export class BaseCodegen {
|
|
|
157
157
|
case 'TypeAlias': result = this.genTypeAlias(node); break;
|
|
158
158
|
case 'DeferStatement': result = this.genDeferStatement(node); break;
|
|
159
159
|
case 'ExternDeclaration': result = `${this.i()}// extern: ${node.name}`; break;
|
|
160
|
+
// Config declarations handled at block level — emit nothing in statement context
|
|
161
|
+
case 'AiConfigDeclaration': result = ''; break;
|
|
162
|
+
case 'DataBlock': result = ''; break;
|
|
163
|
+
case 'SourceDeclaration': result = ''; break;
|
|
164
|
+
case 'PipelineDeclaration': result = ''; break;
|
|
165
|
+
case 'ValidateBlock': result = ''; break;
|
|
166
|
+
case 'RefreshPolicy': result = ''; break;
|
|
167
|
+
case 'RefinementType': result = this.genRefinementType(node); break;
|
|
160
168
|
default:
|
|
161
169
|
result = `${this.i()}${this.genExpression(node)};`;
|
|
162
170
|
}
|
|
@@ -213,6 +221,10 @@ export class BaseCodegen {
|
|
|
213
221
|
case 'AwaitExpression': return `(await ${this.genExpression(node.argument)})`;
|
|
214
222
|
case 'YieldExpression': return node.delegate ? `(yield* ${this.genExpression(node.argument)})` : `(yield ${this.genExpression(node.argument)})`;
|
|
215
223
|
case 'TupleExpression': return `[${node.elements.map(e => this.genExpression(e)).join(', ')}]`;
|
|
224
|
+
// Column expressions (for table operations)
|
|
225
|
+
case 'ColumnExpression': return this.genColumnExpression(node);
|
|
226
|
+
case 'ColumnAssignment': return this.genColumnAssignment(node);
|
|
227
|
+
case 'NegatedColumnExpression': return `{ __exclude: ${JSON.stringify(node.name)} }`;
|
|
216
228
|
default:
|
|
217
229
|
throw new Error(`Codegen: unknown expression type '${node.type}'`);
|
|
218
230
|
}
|
|
@@ -754,6 +766,20 @@ export class BaseCodegen {
|
|
|
754
766
|
if (node.callee.name === 'Ok' || node.callee.name === 'Err' || node.callee.name === 'Some') {
|
|
755
767
|
this._needsResultOption = true;
|
|
756
768
|
}
|
|
769
|
+
|
|
770
|
+
// Inline string/collection builtins to direct method calls
|
|
771
|
+
const inlined = this._tryInlineBuiltin(node);
|
|
772
|
+
if (inlined !== null) return inlined;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Check for table operation calls with column expressions
|
|
776
|
+
const hasColumnExprs = node.arguments.some(a => this._containsColumnExpr(a));
|
|
777
|
+
if (hasColumnExprs || (node.callee.type === 'Identifier' && ['agg', 'table_agg'].includes(node.callee.name))) {
|
|
778
|
+
const tableArgs = this._genTableCallArgs(node);
|
|
779
|
+
if (tableArgs) {
|
|
780
|
+
const callee = this.genExpression(node.callee);
|
|
781
|
+
return `${callee}(${tableArgs.join(', ')})`;
|
|
782
|
+
}
|
|
757
783
|
}
|
|
758
784
|
|
|
759
785
|
const callee = this.genExpression(node.callee);
|
|
@@ -783,6 +809,85 @@ export class BaseCodegen {
|
|
|
783
809
|
return `${callee}(${args})`;
|
|
784
810
|
}
|
|
785
811
|
|
|
812
|
+
// Inline known builtins to direct method calls, eliminating wrapper overhead.
|
|
813
|
+
// Returns the inlined code string, or null if not inlineable.
|
|
814
|
+
_tryInlineBuiltin(node) {
|
|
815
|
+
const name = node.callee.name;
|
|
816
|
+
const args = node.arguments;
|
|
817
|
+
|
|
818
|
+
switch (name) {
|
|
819
|
+
// String methods: fn(str, ...) → str.method(...)
|
|
820
|
+
case 'split':
|
|
821
|
+
if (args.length === 2)
|
|
822
|
+
return `${this.genExpression(args[0])}.split(${this.genExpression(args[1])})`;
|
|
823
|
+
break;
|
|
824
|
+
case 'join':
|
|
825
|
+
if (args.length === 2)
|
|
826
|
+
return `${this.genExpression(args[0])}.join(${this.genExpression(args[1])})`;
|
|
827
|
+
if (args.length === 1)
|
|
828
|
+
return `${this.genExpression(args[0])}.join('')`;
|
|
829
|
+
break;
|
|
830
|
+
case 'replace':
|
|
831
|
+
if (args.length === 3)
|
|
832
|
+
return `${this.genExpression(args[0])}.replaceAll(${this.genExpression(args[1])}, ${this.genExpression(args[2])})`;
|
|
833
|
+
break;
|
|
834
|
+
case 'contains':
|
|
835
|
+
if (args.length === 2)
|
|
836
|
+
return `${this.genExpression(args[0])}.includes(${this.genExpression(args[1])})`;
|
|
837
|
+
break;
|
|
838
|
+
case 'upper':
|
|
839
|
+
if (args.length === 1)
|
|
840
|
+
return `${this.genExpression(args[0])}.toUpperCase()`;
|
|
841
|
+
break;
|
|
842
|
+
case 'lower':
|
|
843
|
+
if (args.length === 1)
|
|
844
|
+
return `${this.genExpression(args[0])}.toLowerCase()`;
|
|
845
|
+
break;
|
|
846
|
+
case 'trim':
|
|
847
|
+
if (args.length === 1)
|
|
848
|
+
return `${this.genExpression(args[0])}.trim()`;
|
|
849
|
+
break;
|
|
850
|
+
case 'trim_start':
|
|
851
|
+
if (args.length === 1)
|
|
852
|
+
return `${this.genExpression(args[0])}.trimStart()`;
|
|
853
|
+
break;
|
|
854
|
+
case 'trim_end':
|
|
855
|
+
if (args.length === 1)
|
|
856
|
+
return `${this.genExpression(args[0])}.trimEnd()`;
|
|
857
|
+
break;
|
|
858
|
+
case 'repeat':
|
|
859
|
+
if (args.length === 2)
|
|
860
|
+
return `${this.genExpression(args[0])}.repeat(${this.genExpression(args[1])})`;
|
|
861
|
+
break;
|
|
862
|
+
case 'starts_with':
|
|
863
|
+
if (args.length === 2)
|
|
864
|
+
return `${this.genExpression(args[0])}.startsWith(${this.genExpression(args[1])})`;
|
|
865
|
+
break;
|
|
866
|
+
case 'ends_with':
|
|
867
|
+
if (args.length === 2)
|
|
868
|
+
return `${this.genExpression(args[0])}.endsWith(${this.genExpression(args[1])})`;
|
|
869
|
+
break;
|
|
870
|
+
case 'pad_start':
|
|
871
|
+
if (args.length >= 2)
|
|
872
|
+
return `${this.genExpression(args[0])}.padStart(${this.genExpression(args[1])}${args[2] ? ', ' + this.genExpression(args[2]) : ''})`;
|
|
873
|
+
break;
|
|
874
|
+
case 'pad_end':
|
|
875
|
+
if (args.length >= 2)
|
|
876
|
+
return `${this.genExpression(args[0])}.padEnd(${this.genExpression(args[1])}${args[2] ? ', ' + this.genExpression(args[2]) : ''})`;
|
|
877
|
+
break;
|
|
878
|
+
case 'includes':
|
|
879
|
+
if (args.length === 2)
|
|
880
|
+
return `${this.genExpression(args[0])}.includes(${this.genExpression(args[1])})`;
|
|
881
|
+
break;
|
|
882
|
+
case 'char_at':
|
|
883
|
+
if (args.length === 2)
|
|
884
|
+
return `${this.genExpression(args[0])}[${this.genExpression(args[1])}]`;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
|
|
786
891
|
genMemberExpression(node) {
|
|
787
892
|
const obj = this.genExpression(node.object);
|
|
788
893
|
if (node.computed) {
|
|
@@ -814,6 +919,20 @@ export class BaseCodegen {
|
|
|
814
919
|
|
|
815
920
|
// If right is a call expression, check for placeholder _ or insert as first arg
|
|
816
921
|
if (right.type === 'CallExpression') {
|
|
922
|
+
// Check for table operations with column expressions
|
|
923
|
+
const hasColumnExprs = right.arguments.some(a => this._containsColumnExpr(a));
|
|
924
|
+
if (hasColumnExprs || (right.callee.type === 'Identifier' && ['agg', 'table_agg'].includes(right.callee.name))) {
|
|
925
|
+
const tableArgs = this._genTableCallArgs(right);
|
|
926
|
+
if (tableArgs) {
|
|
927
|
+
const callee = this.genExpression(right.callee);
|
|
928
|
+
// Track builtin usage
|
|
929
|
+
if (right.callee.type === 'Identifier' && BUILTIN_NAMES.has(right.callee.name)) {
|
|
930
|
+
this._usedBuiltins.add(right.callee.name);
|
|
931
|
+
}
|
|
932
|
+
return `${callee}(${[left, ...tableArgs].join(', ')})`;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
817
936
|
const placeholderCount = right.arguments.filter(a => a.type === 'Identifier' && a.name === '_').length;
|
|
818
937
|
if (placeholderCount > 0) {
|
|
819
938
|
const callee = this.genExpression(right.callee);
|
|
@@ -849,6 +968,250 @@ export class BaseCodegen {
|
|
|
849
968
|
return `(${this.genExpression(right)})(${left})`;
|
|
850
969
|
}
|
|
851
970
|
|
|
971
|
+
// ─── Column expressions ──────────────────────────────────
|
|
972
|
+
|
|
973
|
+
// Context flag for whether we're inside a select() call argument list
|
|
974
|
+
_columnAsString = false;
|
|
975
|
+
|
|
976
|
+
genColumnExpression(node) {
|
|
977
|
+
// Inside select(), column expressions compile to string names
|
|
978
|
+
if (this._columnAsString) {
|
|
979
|
+
return JSON.stringify(node.name);
|
|
980
|
+
}
|
|
981
|
+
// Default: compile to row lambda
|
|
982
|
+
return `(__row) => __row.${node.name}`;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
genColumnAssignment(node) {
|
|
986
|
+
// Compile to object entry for derive(): { colName: (row) => expr }
|
|
987
|
+
// The expression inside the assignment may reference other columns
|
|
988
|
+
const expr = this._genColumnBody(node.expression);
|
|
989
|
+
return `${JSON.stringify(node.target)}: (__row) => ${expr}`;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Generate an expression body that wraps column references as __row.col
|
|
993
|
+
_genColumnBody(node) {
|
|
994
|
+
if (!node) return 'undefined';
|
|
995
|
+
if (node.type === 'ColumnExpression') {
|
|
996
|
+
return `__row.${node.name}`;
|
|
997
|
+
}
|
|
998
|
+
if (node.type === 'BinaryExpression') {
|
|
999
|
+
return `(${this._genColumnBody(node.left)} ${node.operator} ${this._genColumnBody(node.right)})`;
|
|
1000
|
+
}
|
|
1001
|
+
if (node.type === 'LogicalExpression') {
|
|
1002
|
+
const op = node.operator === 'and' ? '&&' : node.operator === 'or' ? '||' : node.operator;
|
|
1003
|
+
return `(${this._genColumnBody(node.left)} ${op} ${this._genColumnBody(node.right)})`;
|
|
1004
|
+
}
|
|
1005
|
+
if (node.type === 'UnaryExpression') {
|
|
1006
|
+
return `${node.operator}${this._genColumnBody(node.operand)}`;
|
|
1007
|
+
}
|
|
1008
|
+
if (node.type === 'CallExpression') {
|
|
1009
|
+
// Check if callee is a builtin used as pipe target
|
|
1010
|
+
const callee = this.genExpression(node.callee);
|
|
1011
|
+
const args = node.arguments.map(a => this._genColumnBody(a)).join(', ');
|
|
1012
|
+
return `${callee}(${args})`;
|
|
1013
|
+
}
|
|
1014
|
+
if (node.type === 'PipeExpression') {
|
|
1015
|
+
const left = this._genColumnBody(node.left);
|
|
1016
|
+
const right = node.right;
|
|
1017
|
+
if (right.type === 'CallExpression') {
|
|
1018
|
+
const callee = this.genExpression(right.callee);
|
|
1019
|
+
const args = [left, ...right.arguments.map(a => this._genColumnBody(a))].join(', ');
|
|
1020
|
+
return `${callee}(${args})`;
|
|
1021
|
+
}
|
|
1022
|
+
if (right.type === 'Identifier') {
|
|
1023
|
+
return `${right.name}(${left})`;
|
|
1024
|
+
}
|
|
1025
|
+
return `(${this._genColumnBody(right)})(${left})`;
|
|
1026
|
+
}
|
|
1027
|
+
if (node.type === 'TemplateLiteral') {
|
|
1028
|
+
// Template literal with column references
|
|
1029
|
+
const parts = node.parts.map(p => {
|
|
1030
|
+
if (p.type === 'text') return p.value;
|
|
1031
|
+
return `\${${this._genColumnBody(p.value)}}`;
|
|
1032
|
+
});
|
|
1033
|
+
return '`' + parts.join('') + '`';
|
|
1034
|
+
}
|
|
1035
|
+
if (node.type === 'ConditionalExpression' || node.type === 'IfExpression') {
|
|
1036
|
+
const cond = this._genColumnBody(node.condition);
|
|
1037
|
+
const cons = this._genColumnBody(node.consequent);
|
|
1038
|
+
const alt = node.alternate || node.elseBody;
|
|
1039
|
+
const altCode = alt ? this._genColumnBody(alt) : 'undefined';
|
|
1040
|
+
return `(${cond} ? ${cons} : ${altCode})`;
|
|
1041
|
+
}
|
|
1042
|
+
if (node.type === 'MatchExpression') {
|
|
1043
|
+
// Match on column value
|
|
1044
|
+
const subject = this._genColumnBody(node.subject);
|
|
1045
|
+
const tmp = `__match_${this._uid()}`;
|
|
1046
|
+
let code = `((__m) => { `;
|
|
1047
|
+
for (const arm of node.arms) {
|
|
1048
|
+
if (arm.pattern.type === 'WildcardPattern') {
|
|
1049
|
+
const body = this._genColumnBody(arm.body);
|
|
1050
|
+
code += `return ${body}; `;
|
|
1051
|
+
} else if (arm.pattern.type === 'RangePattern') {
|
|
1052
|
+
const start = this.genExpression(arm.pattern.start);
|
|
1053
|
+
const end = this.genExpression(arm.pattern.end);
|
|
1054
|
+
const op = arm.pattern.inclusive ? '<=' : '<';
|
|
1055
|
+
code += `if (__m >= ${start} && __m ${op} ${end}) return ${this._genColumnBody(arm.body)}; `;
|
|
1056
|
+
} else {
|
|
1057
|
+
const pat = this.genExpression(arm.pattern.value || arm.pattern);
|
|
1058
|
+
code += `if (__m === ${pat}) return ${this._genColumnBody(arm.body)}; `;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
code += `})(${subject})`;
|
|
1062
|
+
return code;
|
|
1063
|
+
}
|
|
1064
|
+
if (node.type === 'MemberExpression') {
|
|
1065
|
+
const obj = this._genColumnBody(node.object);
|
|
1066
|
+
if (node.computed) {
|
|
1067
|
+
return `${obj}[${this._genColumnBody(node.property)}]`;
|
|
1068
|
+
}
|
|
1069
|
+
return `${obj}.${node.property}`;
|
|
1070
|
+
}
|
|
1071
|
+
// Fallback to normal expression generation for constants, strings, etc.
|
|
1072
|
+
return this.genExpression(node);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Override genCallExpression to handle table operations with column expressions
|
|
1076
|
+
_genTableCallArgs(node) {
|
|
1077
|
+
const calleeName = node.callee.type === 'Identifier' ? node.callee.name : null;
|
|
1078
|
+
|
|
1079
|
+
// select() — column expressions should compile to strings
|
|
1080
|
+
if (calleeName === 'select' || calleeName === 'table_select') {
|
|
1081
|
+
this._columnAsString = true;
|
|
1082
|
+
const args = node.arguments.map(a => this.genExpression(a));
|
|
1083
|
+
this._columnAsString = false;
|
|
1084
|
+
return args;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// where() — column expressions compile to row lambdas
|
|
1088
|
+
if (calleeName === 'where' || calleeName === 'table_where') {
|
|
1089
|
+
return node.arguments.map(a => {
|
|
1090
|
+
if (this._containsColumnExpr(a)) {
|
|
1091
|
+
return `(__row) => ${this._genColumnBody(a)}`;
|
|
1092
|
+
}
|
|
1093
|
+
return this.genExpression(a);
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// sort_by() — column expression compiles to row lambda
|
|
1098
|
+
if (calleeName === 'sort_by' || calleeName === 'table_sort_by') {
|
|
1099
|
+
return node.arguments.map(a => {
|
|
1100
|
+
if (this._containsColumnExpr(a)) {
|
|
1101
|
+
return `(__row) => ${this._genColumnBody(a)}`;
|
|
1102
|
+
}
|
|
1103
|
+
return this.genExpression(a);
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// group_by() — column expression compiles to row lambda
|
|
1108
|
+
if (calleeName === 'group_by' || calleeName === 'table_group_by') {
|
|
1109
|
+
return node.arguments.map(a => {
|
|
1110
|
+
if (this._containsColumnExpr(a)) {
|
|
1111
|
+
return `(__row) => ${this._genColumnBody(a)}`;
|
|
1112
|
+
}
|
|
1113
|
+
return this.genExpression(a);
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// derive() — column assignments compile to { name: (row) => expr }
|
|
1118
|
+
if (calleeName === 'derive' || calleeName === 'table_derive') {
|
|
1119
|
+
const parts = [];
|
|
1120
|
+
for (const a of node.arguments) {
|
|
1121
|
+
if (a.type === 'ColumnAssignment') {
|
|
1122
|
+
parts.push(this.genColumnAssignment(a));
|
|
1123
|
+
} else {
|
|
1124
|
+
parts.push(this.genExpression(a));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
// Wrap column assignments in an object
|
|
1128
|
+
const hasAssignments = node.arguments.some(a => a.type === 'ColumnAssignment');
|
|
1129
|
+
if (hasAssignments) {
|
|
1130
|
+
return [`{ ${parts.join(', ')} }`];
|
|
1131
|
+
}
|
|
1132
|
+
return parts;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// agg() — named arguments with aggregation functions
|
|
1136
|
+
if (calleeName === 'agg' || calleeName === 'table_agg') {
|
|
1137
|
+
const parts = [];
|
|
1138
|
+
for (const a of node.arguments) {
|
|
1139
|
+
if (a.type === 'NamedArgument') {
|
|
1140
|
+
// Named agg: total: sum(.amount) → total: agg_sum((__row) => __row.amount)
|
|
1141
|
+
const val = a.value;
|
|
1142
|
+
if (val.type === 'CallExpression' && val.callee.type === 'Identifier') {
|
|
1143
|
+
const aggName = val.callee.name;
|
|
1144
|
+
const aggFn = `agg_${aggName}`;
|
|
1145
|
+
if (['sum', 'count', 'mean', 'median', 'min', 'max'].includes(aggName)) {
|
|
1146
|
+
this._usedBuiltins.add(aggFn);
|
|
1147
|
+
if (val.arguments.length === 0) {
|
|
1148
|
+
// count() with no args
|
|
1149
|
+
parts.push(`${a.name}: ${aggFn}()`);
|
|
1150
|
+
} else {
|
|
1151
|
+
const inner = val.arguments[0];
|
|
1152
|
+
if (this._containsColumnExpr(inner)) {
|
|
1153
|
+
parts.push(`${a.name}: ${aggFn}((__row) => ${this._genColumnBody(inner)})`);
|
|
1154
|
+
} else {
|
|
1155
|
+
parts.push(`${a.name}: ${aggFn}(${this.genExpression(inner)})`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
parts.push(`${a.name}: ${this.genExpression(a.value)}`);
|
|
1162
|
+
} else {
|
|
1163
|
+
parts.push(this.genExpression(a));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const hasNamed = node.arguments.some(a => a.type === 'NamedArgument');
|
|
1167
|
+
if (hasNamed) {
|
|
1168
|
+
return [`{ ${parts.join(', ')} }`];
|
|
1169
|
+
}
|
|
1170
|
+
return parts;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// drop_nil/fill_nil — column expression compiles to string or lambda
|
|
1174
|
+
if (calleeName === 'drop_nil' || calleeName === 'fill_nil') {
|
|
1175
|
+
return node.arguments.map(a => {
|
|
1176
|
+
if (a.type === 'ColumnExpression') {
|
|
1177
|
+
return JSON.stringify(a.name);
|
|
1178
|
+
}
|
|
1179
|
+
return this.genExpression(a);
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// join() — handle left/right column expressions
|
|
1184
|
+
if (calleeName === 'join' || calleeName === 'table_join') {
|
|
1185
|
+
return node.arguments.map(a => {
|
|
1186
|
+
if (a.type === 'NamedArgument') {
|
|
1187
|
+
if ((a.name === 'left' || a.name === 'right') && a.value.type === 'ColumnExpression') {
|
|
1188
|
+
return this.genExpression(a); // NamedArgument genExpression handles it
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return this.genExpression(a);
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return null; // No special handling needed
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
_containsColumnExpr(node) {
|
|
1199
|
+
if (!node) return false;
|
|
1200
|
+
if (node.type === 'ColumnExpression' || node.type === 'ColumnAssignment' || node.type === 'NegatedColumnExpression') return true;
|
|
1201
|
+
for (const key of Object.keys(node)) {
|
|
1202
|
+
if (key === 'loc' || key === 'type') continue;
|
|
1203
|
+
const val = node[key];
|
|
1204
|
+
if (Array.isArray(val)) {
|
|
1205
|
+
for (const item of val) {
|
|
1206
|
+
if (item && typeof item === 'object' && this._containsColumnExpr(item)) return true;
|
|
1207
|
+
}
|
|
1208
|
+
} else if (val && typeof val === 'object' && val.type) {
|
|
1209
|
+
if (this._containsColumnExpr(val)) return true;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
852
1215
|
genLambdaExpression(node) {
|
|
853
1216
|
const params = this.genParams(node.params);
|
|
854
1217
|
const hasPropagate = this._containsPropagate(node.body);
|
|
@@ -1323,6 +1686,14 @@ export class BaseCodegen {
|
|
|
1323
1686
|
return `${this.i()}/* ${exportStr}type alias: ${node.name} = ${typeStr} */`;
|
|
1324
1687
|
}
|
|
1325
1688
|
|
|
1689
|
+
genRefinementType(node) {
|
|
1690
|
+
// Refinement types compile to validator functions
|
|
1691
|
+
// type Email = String where { it |> contains("@") }
|
|
1692
|
+
// → function __validate_Email(it) { return it.includes("@"); }
|
|
1693
|
+
const predExpr = this.genExpression(node.predicate);
|
|
1694
|
+
return `${this.i()}function __validate_${node.name}(it) {\n${this.i()} if (!(${predExpr})) throw new Error("Refinement type ${node.name} validation failed");\n${this.i()} return it;\n${this.i()}}`;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1326
1697
|
genDeferStatement(node) {
|
|
1327
1698
|
// Defer is handled by genBlockBody which collects defers and wraps in try/finally.
|
|
1328
1699
|
// If called outside genBlockBody (e.g., via genBlockStatements), generate a no-op comment.
|
|
@@ -172,24 +172,48 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
172
172
|
return `${asyncPrefix}(${params}) => ${this.genExpression(node.body)}`;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
generate(clientBlocks, sharedCode) {
|
|
175
|
+
generate(clientBlocks, sharedCode, sharedBuiltins = null) {
|
|
176
|
+
this._sharedBuiltins = sharedBuiltins || new Set();
|
|
176
177
|
const lines = [];
|
|
177
178
|
|
|
178
179
|
// Runtime imports
|
|
179
180
|
lines.push(`import { createSignal, createEffect, createComputed, mount, hydrate, tova_el, tova_fragment, tova_keyed, tova_inject_css, batch, onMount, onUnmount, onCleanup, createRef, createContext, provide, inject, createErrorBoundary, ErrorBoundary, createRoot, watch, untrack, Dynamic, Portal, lazy } from './runtime/reactivity.js';`);
|
|
180
181
|
lines.push(`import { rpc } from './runtime/rpc.js';`);
|
|
181
|
-
lines.push('');
|
|
182
182
|
|
|
183
|
-
//
|
|
183
|
+
// Hoist import lines from shared code to the top of the module
|
|
184
|
+
let sharedRest = sharedCode;
|
|
184
185
|
if (sharedCode.trim()) {
|
|
186
|
+
const sharedLines = sharedCode.split('\n');
|
|
187
|
+
const importLines = [];
|
|
188
|
+
const nonImportLines = [];
|
|
189
|
+
for (const line of sharedLines) {
|
|
190
|
+
if (/^\s*import\s+/.test(line)) {
|
|
191
|
+
importLines.push(line);
|
|
192
|
+
} else {
|
|
193
|
+
nonImportLines.push(line);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (importLines.length > 0) {
|
|
197
|
+
for (const imp of importLines) {
|
|
198
|
+
lines.push(imp);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
sharedRest = nonImportLines.join('\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
lines.push('');
|
|
205
|
+
|
|
206
|
+
// Shared code (non-import lines)
|
|
207
|
+
if (sharedRest.trim()) {
|
|
185
208
|
lines.push('// ── Shared ──');
|
|
186
|
-
lines.push(
|
|
209
|
+
lines.push(sharedRest);
|
|
187
210
|
lines.push('');
|
|
188
211
|
}
|
|
189
212
|
|
|
190
|
-
// Stdlib
|
|
213
|
+
// Stdlib placeholder — filled after all client code is generated so tree-shaking sees all usages
|
|
214
|
+
const stdlibPlaceholderIdx = lines.length;
|
|
191
215
|
lines.push('// ── Stdlib ──');
|
|
192
|
-
lines.push(
|
|
216
|
+
lines.push('__STDLIB_PLACEHOLDER__');
|
|
193
217
|
lines.push('');
|
|
194
218
|
|
|
195
219
|
// Server RPC proxy
|
|
@@ -206,6 +230,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
206
230
|
const effects = [];
|
|
207
231
|
const components = [];
|
|
208
232
|
const stores = [];
|
|
233
|
+
const imports = [];
|
|
209
234
|
const other = [];
|
|
210
235
|
|
|
211
236
|
for (const block of clientBlocks) {
|
|
@@ -216,11 +241,23 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
216
241
|
case 'EffectDeclaration': effects.push(stmt); break;
|
|
217
242
|
case 'ComponentDeclaration': components.push(stmt); break;
|
|
218
243
|
case 'StoreDeclaration': stores.push(stmt); break;
|
|
244
|
+
case 'ImportDeclaration': imports.push(stmt); break;
|
|
245
|
+
case 'ImportDefault': imports.push(stmt); break;
|
|
246
|
+
case 'ImportWildcard': imports.push(stmt); break;
|
|
219
247
|
default: other.push(stmt); break;
|
|
220
248
|
}
|
|
221
249
|
}
|
|
222
250
|
}
|
|
223
251
|
|
|
252
|
+
// Generate client block imports (hoisted after runtime imports)
|
|
253
|
+
if (imports.length > 0) {
|
|
254
|
+
lines.push('// ── Client Imports ──');
|
|
255
|
+
for (const stmt of imports) {
|
|
256
|
+
lines.push(this.generateStatement(stmt));
|
|
257
|
+
}
|
|
258
|
+
lines.push('');
|
|
259
|
+
}
|
|
260
|
+
|
|
224
261
|
// Register state names for setter transforms
|
|
225
262
|
for (const s of states) {
|
|
226
263
|
this.stateNames.add(s.name);
|
|
@@ -301,14 +338,23 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
301
338
|
}
|
|
302
339
|
|
|
303
340
|
// Auto-mount the App component if it exists
|
|
341
|
+
// Auto-detect SSR: if the container already has children, hydrate instead of mount
|
|
304
342
|
const hasApp = components.some(c => c.name === 'App');
|
|
305
343
|
if (hasApp) {
|
|
306
344
|
lines.push('// ── Mount ──');
|
|
307
345
|
lines.push('document.addEventListener("DOMContentLoaded", () => {');
|
|
308
|
-
lines.push('
|
|
346
|
+
lines.push(' const container = document.getElementById("app") || document.body;');
|
|
347
|
+
lines.push(' if (container.children.length > 0) {');
|
|
348
|
+
lines.push(' hydrate(App, container);');
|
|
349
|
+
lines.push(' } else {');
|
|
350
|
+
lines.push(' mount(App, container);');
|
|
351
|
+
lines.push(' }');
|
|
309
352
|
lines.push('});');
|
|
310
353
|
}
|
|
311
354
|
|
|
355
|
+
// Replace stdlib placeholder now that all client code has been generated
|
|
356
|
+
lines[stdlibPlaceholderIdx + 1] = this.getStdlibCore();
|
|
357
|
+
|
|
312
358
|
return lines.join('\n');
|
|
313
359
|
}
|
|
314
360
|
|
|
@@ -751,7 +797,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
751
797
|
propsStr = `{${propParts.join(', ')}}`;
|
|
752
798
|
}
|
|
753
799
|
}
|
|
754
|
-
return
|
|
800
|
+
return `(() => { const __v = ${node.tag}(${propsStr}); if (__v && __v.__tova) __v._componentName = "${node.tag}"; return __v; })()`;
|
|
755
801
|
}
|
|
756
802
|
|
|
757
803
|
const tag = JSON.stringify(node.tag);
|
|
@@ -860,8 +906,14 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
860
906
|
|
|
861
907
|
getStdlibCore() {
|
|
862
908
|
const parts = [];
|
|
863
|
-
// Only include used
|
|
864
|
-
const
|
|
909
|
+
// Only include builtins used in client blocks that aren't already in shared code
|
|
910
|
+
const clientOnly = new Set();
|
|
911
|
+
for (const name of this._usedBuiltins) {
|
|
912
|
+
if (!this._sharedBuiltins || !this._sharedBuiltins.has(name)) {
|
|
913
|
+
clientOnly.add(name);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const selectiveStdlib = buildSelectiveStdlib(clientOnly);
|
|
865
917
|
if (selectiveStdlib) parts.push(selectiveStdlib);
|
|
866
918
|
// Include Result/Option if Ok/Err/Some/None are used
|
|
867
919
|
if (this._needsResultOption) parts.push(RESULT_OPTION);
|