tova 0.3.0 → 0.3.1

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.
@@ -156,7 +156,7 @@ export class ServerCodegen extends BaseCodegen {
156
156
  const modelDecls = [];
157
157
  const aiConfigs = []; // { name: string|null, config: object }
158
158
 
159
- const collectFromBody = (stmts, groupPrefix = null, groupMiddlewares = []) => {
159
+ const collectFromBody = (stmts, groupPrefix = null, groupMiddlewares = [], groupVersion = null) => {
160
160
  for (const stmt of stmts) {
161
161
  if (stmt.type === 'RouteDeclaration') {
162
162
  const route = stmt;
@@ -165,6 +165,7 @@ export class ServerCodegen extends BaseCodegen {
165
165
  ...route,
166
166
  path: groupPrefix + route.path,
167
167
  _groupMiddlewares: groupMiddlewares.length > 0 ? [...groupMiddlewares] : undefined,
168
+ _version: groupVersion || undefined,
168
169
  };
169
170
  routes.push(prefixedRoute);
170
171
  } else {
@@ -196,7 +197,8 @@ export class ServerCodegen extends BaseCodegen {
196
197
  } else if (stmt.type === 'RouteGroupDeclaration') {
197
198
  const prefix = groupPrefix ? groupPrefix + stmt.prefix : stmt.prefix;
198
199
  const grpMw = [...groupMiddlewares]; // inherit parent group middlewares
199
- collectFromBody(stmt.body, prefix, grpMw);
200
+ const ver = stmt.version || groupVersion; // inherit or override version
201
+ collectFromBody(stmt.body, prefix, grpMw, ver);
200
202
  } else if (stmt.type === 'RateLimitDeclaration') {
201
203
  rateLimitConfig = stmt.config;
202
204
  } else if (stmt.type === 'LifecycleHookDeclaration') {
@@ -240,8 +242,8 @@ export class ServerCodegen extends BaseCodegen {
240
242
 
241
243
  // Collect type declarations from shared blocks for model/ORM generation
242
244
  const sharedTypes = new Map(); // typeName -> { fields: [{ name, type }] }
243
- for (const sb of sharedBlocks) {
244
- for (const stmt of sb.body) {
245
+ const _collectTypes = (stmts) => {
246
+ for (const stmt of stmts) {
245
247
  if (stmt.type === 'TypeDeclaration' && stmt.variants) {
246
248
  const fields = [];
247
249
  for (const v of stmt.variants) {
@@ -254,6 +256,13 @@ export class ServerCodegen extends BaseCodegen {
254
256
  }
255
257
  }
256
258
  }
259
+ };
260
+ for (const sb of sharedBlocks) {
261
+ _collectTypes(sb.body);
262
+ }
263
+ // Also collect types from server blocks (for body: Type validation)
264
+ for (const block of serverBlocks) {
265
+ _collectTypes(block.body);
257
266
  }
258
267
 
259
268
  // Separate group-only middlewares from global middlewares
@@ -788,12 +797,12 @@ export class ServerCodegen extends BaseCodegen {
788
797
  // ════════════════════════════════════════════════════════════
789
798
  lines.push('// ── Router ──');
790
799
  lines.push('const __routes = [];');
791
- lines.push('function __addRoute(method, path, handler) {');
800
+ lines.push('function __addRoute(method, path, handler, version) {');
792
801
  lines.push(' let pattern = path');
793
802
  lines.push(' .replace(/\\*([a-zA-Z_][a-zA-Z0-9_]*)/g, "(?<$1>.+)")');
794
803
  lines.push(' .replace(/\\*$/g, "(.*)")');
795
804
  lines.push(' .replace(/:([^/]+)/g, "(?<$1>[^/]+)");');
796
- lines.push(' __routes.push({ method, regex: new RegExp(`^${pattern}$`), handler, _path: path });');
805
+ lines.push(' __routes.push({ method, regex: new RegExp(`^${pattern}$`), handler, _path: path, _version: version || null });');
797
806
  lines.push('}');
798
807
  lines.push('');
799
808
 
@@ -1699,6 +1708,10 @@ export class ServerCodegen extends BaseCodegen {
1699
1708
  const validateDec = decorators.find(d => d.name === 'validate');
1700
1709
  const uploadDec = decorators.find(d => d.name === 'upload');
1701
1710
 
1711
+ // T9-5: Version info for route
1712
+ const routeVersion = route._version;
1713
+ const versionArg = routeVersion ? `, ${JSON.stringify(String(routeVersion.version || ''))}` : '';
1714
+
1702
1715
  lines.push(`__addRoute(${JSON.stringify(method)}, ${JSON.stringify(path)}, async (req, params) => {`);
1703
1716
 
1704
1717
  // Auth decorator check
@@ -1741,6 +1754,68 @@ export class ServerCodegen extends BaseCodegen {
1741
1754
  lines.push(` if (__validationErrors.length > 0) return Response.json({ error: "Validation failed", details: __validationErrors }, { status: 400 });`);
1742
1755
  }
1743
1756
 
1757
+ // T9-1: Route-level body type validation — route POST "/api/users" body: User => handler
1758
+ if (route.bodyType && ['POST', 'PUT', 'PATCH'].includes(method)) {
1759
+ const typeName = route.bodyType.name || (route.bodyType.elementType && route.bodyType.elementType.name);
1760
+ if (typeName && sharedTypes.has(typeName)) {
1761
+ const typeInfo = sharedTypes.get(typeName);
1762
+ if (!uploadDec && !validateDec) {
1763
+ lines.push(` const __body = (await __parseBody(req)) || {};`);
1764
+ }
1765
+ const isArray = route.bodyType.type === 'ArrayTypeAnnotation';
1766
+ if (isArray) {
1767
+ lines.push(` if (!Array.isArray(__body)) return Response.json({ error: "Request body must be an array of ${typeName}" }, { status: 400 });`);
1768
+ lines.push(` const __bodyTypeErrors = [];`);
1769
+ lines.push(` for (let __i = 0; __i < __body.length; __i++) {`);
1770
+ lines.push(` const __item = __body[__i];`);
1771
+ for (const f of typeInfo.fields) {
1772
+ if (f.name === 'id') continue;
1773
+ switch (f.type) {
1774
+ case 'String':
1775
+ lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "string") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a string\`);`);
1776
+ break;
1777
+ case 'Int':
1778
+ lines.push(` if (__item.${f.name} !== undefined && !Number.isInteger(__item.${f.name})) __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be an integer\`);`);
1779
+ break;
1780
+ case 'Float':
1781
+ lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "number") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a number\`);`);
1782
+ break;
1783
+ case 'Bool':
1784
+ lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "boolean") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a boolean\`);`);
1785
+ break;
1786
+ }
1787
+ }
1788
+ lines.push(` }`);
1789
+ lines.push(` if (__bodyTypeErrors.length > 0) return Response.json({ error: "Validation failed for ${typeName}[]", details: __bodyTypeErrors }, { status: 400 });`);
1790
+ } else {
1791
+ lines.push(` const __bodyTypeErrors = [];`);
1792
+ for (const f of typeInfo.fields) {
1793
+ if (f.name === 'id') continue;
1794
+ switch (f.type) {
1795
+ case 'String':
1796
+ lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "string") __bodyTypeErrors.push("${f.name} must be a string");`);
1797
+ break;
1798
+ case 'Int':
1799
+ lines.push(` if (__body.${f.name} !== undefined && !Number.isInteger(__body.${f.name})) __bodyTypeErrors.push("${f.name} must be an integer");`);
1800
+ break;
1801
+ case 'Float':
1802
+ lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "number") __bodyTypeErrors.push("${f.name} must be a number");`);
1803
+ break;
1804
+ case 'Bool':
1805
+ lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "boolean") __bodyTypeErrors.push("${f.name} must be a boolean");`);
1806
+ break;
1807
+ }
1808
+ }
1809
+ // Check for required fields (non-id fields without defaults)
1810
+ for (const f of typeInfo.fields) {
1811
+ if (f.name === 'id') continue;
1812
+ lines.push(` if (__body.${f.name} === undefined || __body.${f.name} === null) __bodyTypeErrors.push("${f.name} is required");`);
1813
+ }
1814
+ lines.push(` if (__bodyTypeErrors.length > 0) return Response.json({ error: "Validation failed for type ${typeName}", details: __bodyTypeErrors }, { status: 400 });`);
1815
+ }
1816
+ }
1817
+ }
1818
+
1744
1819
  // Type-safe body deserialization: if a param has a shared type annotation, auto-validate
1745
1820
  if (handlerDecl && ['POST', 'PUT', 'PATCH'].includes(method)) {
1746
1821
  for (const p of handlerDecl.params) {
@@ -1857,9 +1932,65 @@ export class ServerCodegen extends BaseCodegen {
1857
1932
  this._emitHandlerCall(lines, `${handler}(req, params)`, timeoutMs);
1858
1933
  }
1859
1934
 
1860
- lines.push(` if (__result instanceof Response) return __result;`);
1861
- lines.push(` return Response.json(__result);`);
1862
- lines.push(`});`);
1935
+ // T9-3: Detect generator-based streaming — if handler uses yield, wrap in streaming response
1936
+ const isGeneratorHandler = route.handler.type === 'Identifier'
1937
+ ? (handlerDecl && this._containsYield(handlerDecl.body))
1938
+ : (route.handler.type === 'FunctionDeclaration' || route.handler.type === 'LambdaExpression')
1939
+ ? this._containsYield(route.handler.body || route.handler)
1940
+ : false;
1941
+
1942
+ if (isGeneratorHandler) {
1943
+ lines.push(` if (__result && typeof __result[Symbol.asyncIterator] === "function") {`);
1944
+ lines.push(` const __encoder = new TextEncoder();`);
1945
+ lines.push(` const __stream = new ReadableStream({`);
1946
+ lines.push(` async start(controller) {`);
1947
+ lines.push(` try {`);
1948
+ lines.push(` for await (const __chunk of __result) {`);
1949
+ lines.push(` const __data = typeof __chunk === "string" ? __chunk : JSON.stringify(__chunk);`);
1950
+ lines.push(` controller.enqueue(__encoder.encode(\`data: \${__data}\\n\\n\`));`);
1951
+ lines.push(` }`);
1952
+ lines.push(` } finally { controller.close(); }`);
1953
+ lines.push(` }`);
1954
+ lines.push(` });`);
1955
+ lines.push(` return new Response(__stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" } });`);
1956
+ lines.push(` }`);
1957
+ lines.push(` if (__result && typeof __result[Symbol.iterator] === "function" && typeof __result !== "string") {`);
1958
+ lines.push(` const __encoder = new TextEncoder();`);
1959
+ lines.push(` const __chunks = [...__result];`);
1960
+ lines.push(` const __stream = new ReadableStream({`);
1961
+ lines.push(` start(controller) {`);
1962
+ lines.push(` for (const __chunk of __chunks) {`);
1963
+ lines.push(` const __data = typeof __chunk === "string" ? __chunk : JSON.stringify(__chunk);`);
1964
+ lines.push(` controller.enqueue(__encoder.encode(\`data: \${__data}\\n\\n\`));`);
1965
+ lines.push(` }`);
1966
+ lines.push(` controller.close();`);
1967
+ lines.push(` }`);
1968
+ lines.push(` });`);
1969
+ lines.push(` return new Response(__stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" } });`);
1970
+ lines.push(` }`);
1971
+ }
1972
+ // T9-5: API versioning — add version headers to responses
1973
+ if (routeVersion) {
1974
+ const ver = JSON.stringify(String(routeVersion.version || ''));
1975
+ lines.push(` const __addVersionHeaders = (res) => {`);
1976
+ lines.push(` const h = new Headers(res.headers);`);
1977
+ lines.push(` h.set("API-Version", ${ver});`);
1978
+ if (routeVersion.deprecated) {
1979
+ lines.push(` h.set("Deprecation", "true");`);
1980
+ if (routeVersion.sunset) {
1981
+ lines.push(` h.set("Sunset", ${JSON.stringify(String(routeVersion.sunset))});`);
1982
+ }
1983
+ lines.push(` h.set("Link", '</api/v' + (parseInt(${ver}) + 1) + req.url.replace(/\\/api\\/v\\d+/, "") + '>; rel="successor-version"');`);
1984
+ }
1985
+ lines.push(` return new Response(res.body, { status: res.status, headers: h });`);
1986
+ lines.push(` };`);
1987
+ lines.push(` if (__result instanceof Response) return __addVersionHeaders(__result);`);
1988
+ lines.push(` return __addVersionHeaders(Response.json(__result));`);
1989
+ } else {
1990
+ lines.push(` if (__result instanceof Response) return __result;`);
1991
+ lines.push(` return Response.json(__result);`);
1992
+ }
1993
+ lines.push(`}${versionArg});`);
1863
1994
  lines.push('');
1864
1995
  }
1865
1996
  }
@@ -1928,46 +2059,66 @@ export class ServerCodegen extends BaseCodegen {
1928
2059
  lines.push(' ],');
1929
2060
  }
1930
2061
 
1931
- // Request body schema for POST/PUT/PATCH
1932
- if (['post', 'put', 'patch'].includes(method) && handlerDecl) {
1933
- const bodyParams = handlerDecl.params.filter(p => p.name !== 'req' && !pathParams.includes(p.name));
1934
- if (bodyParams.length > 0) {
1935
- lines.push(' requestBody: {');
1936
- lines.push(' content: { "application/json": { schema: { type: "object", properties: {');
1937
- for (const bp of bodyParams) {
1938
- const ta = bp.typeAnnotation;
1939
- if (ta && ta.name && sharedTypes.has(ta.name)) {
1940
- lines.push(` ${bp.name}: { "$ref": "#/components/schemas/${ta.name}" },`);
1941
- } else if (ta) {
1942
- let jsonType;
1943
- switch (ta.name) {
1944
- case 'Int': jsonType = '"integer"'; break;
1945
- case 'Float': jsonType = '"number"'; break;
1946
- case 'Bool': jsonType = '"boolean"'; break;
1947
- default: jsonType = '"string"'; break;
1948
- }
1949
- lines.push(` ${bp.name}: { type: ${jsonType} },`);
2062
+ // Request body schema for POST/PUT/PATCH — prefer route-level bodyType, fall back to handler params
2063
+ if (['post', 'put', 'patch'].includes(method)) {
2064
+ if (route.bodyType) {
2065
+ // T9-1: Route-level body type annotation
2066
+ const bt = route.bodyType;
2067
+ if (bt.type === 'ArrayTypeAnnotation' && bt.elementType) {
2068
+ const elName = bt.elementType.name;
2069
+ if (sharedTypes.has(elName)) {
2070
+ lines.push(` requestBody: { required: true, content: { "application/json": { schema: { type: "array", items: { "$ref": "#/components/schemas/${elName}" } } } } },`);
1950
2071
  } else {
1951
- lines.push(` ${bp.name}: { type: "string" },`);
2072
+ lines.push(` requestBody: { required: true, content: { "application/json": { schema: { type: "array", items: ${tovaTypeToJsonSchema(elName)} } } } },`);
2073
+ }
2074
+ } else if (bt.type === 'TypeAnnotation' && sharedTypes.has(bt.name)) {
2075
+ lines.push(` requestBody: { required: true, content: { "application/json": { schema: { "$ref": "#/components/schemas/${bt.name}" } } } },`);
2076
+ } else if (bt.type === 'TypeAnnotation') {
2077
+ lines.push(` requestBody: { required: true, content: { "application/json": { schema: ${tovaTypeToJsonSchema(bt.name)} } } },`);
2078
+ }
2079
+ } else if (handlerDecl) {
2080
+ const bodyParams = handlerDecl.params.filter(p => p.name !== 'req' && !pathParams.includes(p.name));
2081
+ if (bodyParams.length > 0) {
2082
+ lines.push(' requestBody: {');
2083
+ lines.push(' content: { "application/json": { schema: { type: "object", properties: {');
2084
+ for (const bp of bodyParams) {
2085
+ const ta = bp.typeAnnotation;
2086
+ if (ta && ta.name && sharedTypes.has(ta.name)) {
2087
+ lines.push(` ${bp.name}: { "$ref": "#/components/schemas/${ta.name}" },`);
2088
+ } else if (ta) {
2089
+ let jsonType;
2090
+ switch (ta.name) {
2091
+ case 'Int': jsonType = '"integer"'; break;
2092
+ case 'Float': jsonType = '"number"'; break;
2093
+ case 'Bool': jsonType = '"boolean"'; break;
2094
+ default: jsonType = '"string"'; break;
2095
+ }
2096
+ lines.push(` ${bp.name}: { type: ${jsonType} },`);
2097
+ } else {
2098
+ lines.push(` ${bp.name}: { type: "string" },`);
2099
+ }
1952
2100
  }
2101
+ lines.push(' } } } },');
2102
+ lines.push(' },');
1953
2103
  }
1954
- lines.push(' } } } },');
1955
- lines.push(' },');
1956
2104
  }
1957
2105
  }
1958
2106
 
1959
- // Response schema from return type
1960
- if (handlerDecl && handlerDecl.returnType) {
1961
- const rt = handlerDecl.returnType;
2107
+ // Response schema prefer route-level responseType (T9-2), fall back to handler return type
2108
+ const responseType = route.responseType || (handlerDecl && handlerDecl.returnType);
2109
+ if (responseType) {
2110
+ const rt = responseType;
1962
2111
  if (rt.type === 'ArrayTypeAnnotation' && rt.elementType) {
1963
2112
  const elName = rt.elementType.name;
1964
2113
  if (sharedTypes.has(elName)) {
1965
2114
  lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: { "$ref": "#/components/schemas/${elName}" } } } } } },`);
1966
2115
  } else {
1967
- lines.push(' responses: { "200": { description: "Success" } },');
2116
+ lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: ${tovaTypeToJsonSchema(elName)} } } } } },`);
1968
2117
  }
1969
2118
  } else if (rt.type === 'TypeAnnotation' && sharedTypes.has(rt.name)) {
1970
2119
  lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { "$ref": "#/components/schemas/${rt.name}" } } } } },`);
2120
+ } else if (rt.type === 'TypeAnnotation') {
2121
+ lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: ${tovaTypeToJsonSchema(rt.name)} } } } },`);
1971
2122
  } else {
1972
2123
  lines.push(' responses: { "200": { description: "Success" } },');
1973
2124
  }
@@ -1994,6 +2145,32 @@ export class ServerCodegen extends BaseCodegen {
1994
2145
  lines.push('');
1995
2146
  }
1996
2147
 
2148
+ // T9-5: API versions endpoint — list available versions
2149
+ const versionedRoutes = routes.filter(r => r._version);
2150
+ if (versionedRoutes.length > 0) {
2151
+ const versionMap = new Map();
2152
+ for (const r of versionedRoutes) {
2153
+ const v = String(r._version.version || '');
2154
+ if (!versionMap.has(v)) {
2155
+ versionMap.set(v, { version: v, deprecated: !!r._version.deprecated, sunset: r._version.sunset || null });
2156
+ }
2157
+ }
2158
+ lines.push('// ── API Versions ──');
2159
+ lines.push('__addRoute("GET", "/api/versions", async () => {');
2160
+ lines.push(' return Response.json({');
2161
+ lines.push(' versions: [');
2162
+ for (const [, info] of versionMap) {
2163
+ const parts = [`version: ${JSON.stringify(info.version)}`];
2164
+ if (info.deprecated) parts.push('deprecated: true');
2165
+ if (info.sunset) parts.push(`sunset: ${JSON.stringify(info.sunset)}`);
2166
+ lines.push(` { ${parts.join(', ')} },`);
2167
+ }
2168
+ lines.push(' ]');
2169
+ lines.push(' });');
2170
+ lines.push('});');
2171
+ lines.push('');
2172
+ }
2173
+
1997
2174
  // Include __contains helper if needed
1998
2175
  if (this._needsContainsHelper) {
1999
2176
  lines.push(this.getContainsHelper());
@@ -2527,7 +2704,31 @@ export class ServerCodegen extends BaseCodegen {
2527
2704
 
2528
2705
  for (const block of testBlocks) {
2529
2706
  const name = block.name || 'Tests';
2707
+ const blockTimeout = block.timeout || null;
2530
2708
  lines.push(`describe(${JSON.stringify(name)}, () => {`);
2709
+
2710
+ // Emit beforeEach if defined
2711
+ if (block.beforeEach && block.beforeEach.length > 0) {
2712
+ lines.push(' beforeEach(async () => {');
2713
+ this.pushScope();
2714
+ for (const s of block.beforeEach) {
2715
+ lines.push(' ' + this.generateStatement(s));
2716
+ }
2717
+ this.popScope();
2718
+ lines.push(' });');
2719
+ }
2720
+
2721
+ // Emit afterEach if defined
2722
+ if (block.afterEach && block.afterEach.length > 0) {
2723
+ lines.push(' afterEach(async () => {');
2724
+ this.pushScope();
2725
+ for (const s of block.afterEach) {
2726
+ lines.push(' ' + this.generateStatement(s));
2727
+ }
2728
+ this.popScope();
2729
+ lines.push(' });');
2730
+ }
2731
+
2531
2732
  for (const stmt of block.body) {
2532
2733
  if (stmt.type === 'FunctionDeclaration') {
2533
2734
  const fnName = stmt.name;
@@ -2539,9 +2740,10 @@ export class ServerCodegen extends BaseCodegen {
2539
2740
  }
2540
2741
  const body = this.genBlockBody(stmt.body);
2541
2742
  this.popScope();
2743
+ const timeoutArg = blockTimeout ? `, ${blockTimeout}` : '';
2542
2744
  lines.push(` test(${JSON.stringify(displayName)}, async () => {`);
2543
2745
  lines.push(body);
2544
- lines.push(' });');
2746
+ lines.push(` }${timeoutArg});`);
2545
2747
  } else {
2546
2748
  lines.push(' ' + this.generateStatement(stmt));
2547
2749
  }
@@ -2553,6 +2755,58 @@ export class ServerCodegen extends BaseCodegen {
2553
2755
  return lines.join('\n');
2554
2756
  }
2555
2757
 
2758
+ generateBench(benchBlocks) {
2759
+ const lines = [];
2760
+ lines.push('// ── Tova Benchmark Runner ──');
2761
+ lines.push('');
2762
+ lines.push('async function __runBench(name, fn, runs) {');
2763
+ lines.push(' runs = runs || 100;');
2764
+ lines.push(' // Warmup');
2765
+ lines.push(' for (let i = 0; i < Math.min(10, runs); i++) await fn();');
2766
+ lines.push(' const times = [];');
2767
+ lines.push(' for (let i = 0; i < runs; i++) {');
2768
+ lines.push(' const start = performance.now();');
2769
+ lines.push(' await fn();');
2770
+ lines.push(' times.push(performance.now() - start);');
2771
+ lines.push(' }');
2772
+ lines.push(' times.sort((a, b) => a - b);');
2773
+ lines.push(' const sum = times.reduce((a, b) => a + b, 0);');
2774
+ lines.push(' const mean = sum / times.length;');
2775
+ lines.push(' const p50 = times[Math.floor(times.length * 0.5)];');
2776
+ lines.push(' const p99 = times[Math.floor(times.length * 0.99)];');
2777
+ lines.push(' console.log(`bench ${JSON.stringify(name)}: mean=${mean.toFixed(2)}ms p50=${p50.toFixed(2)}ms p99=${p99.toFixed(2)}ms (${runs} runs)`);');
2778
+ lines.push('}');
2779
+ lines.push('');
2780
+ lines.push('(async () => {');
2781
+
2782
+ for (const block of benchBlocks) {
2783
+ const name = block.name || 'Benchmark';
2784
+ lines.push(` console.log("── ${name.replace(/"/g, '\\"')} ──");`);
2785
+ for (const stmt of block.body) {
2786
+ if (stmt.type === 'FunctionDeclaration') {
2787
+ const fnName = stmt.name;
2788
+ const displayName = fnName.replace(/_/g, ' ');
2789
+ this.pushScope();
2790
+ for (const p of (stmt.params || [])) {
2791
+ const pName = typeof p === 'string' ? p : (p.name || p.identifier);
2792
+ if (pName) this.declareVar(pName);
2793
+ }
2794
+ const body = this.genBlockBody(stmt.body);
2795
+ this.popScope();
2796
+ lines.push(` await __runBench(${JSON.stringify(displayName)}, async () => {`);
2797
+ lines.push(body);
2798
+ lines.push(' });');
2799
+ } else {
2800
+ lines.push(' ' + this.generateStatement(stmt));
2801
+ }
2802
+ }
2803
+ lines.push('');
2804
+ }
2805
+
2806
+ lines.push('})();');
2807
+ return lines.join('\n');
2808
+ }
2809
+
2556
2810
  _getAiRuntime() {
2557
2811
  return `// AI Client Runtime
2558
2812
  function __createAI(config) {