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.
@@ -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
- // Shared code
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(sharedCode);
209
+ lines.push(sharedRest);
187
210
  lines.push('');
188
211
  }
189
212
 
190
- // Stdlib core functions (available in all Tova code)
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(this.getStdlibCore());
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(' mount(App, document.getElementById("app") || document.body);');
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 `${node.tag}(${propsStr})`;
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 builtin functions (tree-shaking)
864
- const selectiveStdlib = buildSelectiveStdlib(this._usedBuiltins);
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);