@mondaydotcomorg/atp-compiler 0.19.22 → 0.19.26

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.
@@ -0,0 +1,179 @@
1
+ import * as t from '@babel/types';
2
+ import _traverse from '@babel/traverse';
3
+ const traverse = typeof (_traverse as any).default === 'function' ? (_traverse as any).default : _traverse;
4
+ import { generateUniqueId } from '../runtime/context.js';
5
+ import { BatchParallelDetector } from './batch-detector.js';
6
+ import { findAllLLMCallExpressions } from './array-transformer-utils.js';
7
+
8
+ /**
9
+ * Transform array method to batch LLM calls while preserving callback logic.
10
+ * Supports multiple LLM calls per callback.
11
+ *
12
+ * This creates a multi-step transformation:
13
+ * 1. Batch all LLM calls in parallel (one batch per unique type:operation)
14
+ * 2. Reconstruct objects using the batched results
15
+ */
16
+ export function transformToBatchWithReconstruction(
17
+ path: any,
18
+ node: t.CallExpression,
19
+ methodName: string,
20
+ callback: t.Function,
21
+ batchDetector: BatchParallelDetector,
22
+ onTransform: () => void
23
+ ): boolean {
24
+ if (methodName !== 'map') {
25
+ return false;
26
+ }
27
+
28
+ const paramName = callback.params[0];
29
+ if (!t.isIdentifier(paramName)) {
30
+ return false;
31
+ }
32
+ const param = paramName.name;
33
+ const array = (node.callee as t.MemberExpression).object;
34
+
35
+ // Find ALL LLM calls
36
+ const llmCalls = findAllLLMCallExpressions(callback.body, batchDetector);
37
+ if (llmCalls.length === 0) {
38
+ return false;
39
+ }
40
+
41
+ const methodId = generateUniqueId(`${methodName}_batch_reconstruct`);
42
+
43
+ const originalIndexParam = callback.params[1];
44
+ const indexVar =
45
+ originalIndexParam && t.isIdentifier(originalIndexParam) ? originalIndexParam.name : '__idx';
46
+
47
+ // Create batch declarations - one per LLM call (in order of appearance)
48
+ // This ensures each call gets its own result array
49
+ const batchDeclarations: t.Statement[] = [];
50
+ const resultVarByCallIndex = new Map<number, string>();
51
+
52
+ for (let i = 0; i < llmCalls.length; i++) {
53
+ const call = llmCalls[i]!;
54
+ const resultsVar = `__batch_results_${i}_${methodId.replace(/[^a-zA-Z0-9]/g, '_')}`;
55
+ resultVarByCallIndex.set(i, resultsVar);
56
+
57
+ const payloadMapper = t.arrowFunctionExpression(
58
+ [t.identifier(param)],
59
+ t.objectExpression([
60
+ t.objectProperty(t.identifier('type'), t.stringLiteral(call.callInfo.type)),
61
+ t.objectProperty(
62
+ t.identifier('operation'),
63
+ t.stringLiteral(call.callInfo.operation)
64
+ ),
65
+ t.objectProperty(t.identifier('payload'), t.cloneNode(call.payloadNode, true)),
66
+ ])
67
+ );
68
+
69
+ const batchCall = t.awaitExpression(
70
+ t.callExpression(
71
+ t.memberExpression(t.identifier('__runtime'), t.identifier('batchParallel')),
72
+ [
73
+ t.callExpression(
74
+ t.memberExpression(t.cloneNode(array, true), t.identifier('map')),
75
+ [payloadMapper]
76
+ ),
77
+ t.stringLiteral(`${methodId}_${i}`),
78
+ ]
79
+ )
80
+ );
81
+
82
+ batchDeclarations.push(
83
+ t.variableDeclaration('const', [
84
+ t.variableDeclarator(t.identifier(resultsVar), batchCall),
85
+ ])
86
+ );
87
+ }
88
+
89
+ // Clone the callback body for reconstruction
90
+ const clonedBody = t.cloneNode(callback.body, true);
91
+
92
+ let traversableNode: t.Statement;
93
+ if (t.isBlockStatement(clonedBody)) {
94
+ traversableNode = t.functionDeclaration(t.identifier('__temp'), [], clonedBody);
95
+ } else {
96
+ traversableNode = t.expressionStatement(clonedBody as t.Expression);
97
+ }
98
+
99
+ // Replace each await expression with the corresponding result access
100
+ // We match calls by comparing their structure to the original calls
101
+ let replacementCount = 0;
102
+
103
+ traverse(t.file(t.program([traversableNode])), {
104
+ AwaitExpression(awaitPath: any) {
105
+ const arg = awaitPath.node.argument;
106
+ if (!t.isCallExpression(arg)) return;
107
+
108
+ const info = batchDetector.extractCallInfo(arg);
109
+ if (!info) return;
110
+
111
+ const key = `${info.type}:${info.operation}`;
112
+
113
+ // Find first unused call with matching key
114
+ let matchedIndex = -1;
115
+ for (let i = 0; i < llmCalls.length; i++) {
116
+ const original = llmCalls[i]!;
117
+ if (original.key === key && resultVarByCallIndex.has(i)) {
118
+ matchedIndex = i;
119
+ break;
120
+ }
121
+ }
122
+
123
+ if (matchedIndex === -1) return;
124
+
125
+ const resultsVar = resultVarByCallIndex.get(matchedIndex);
126
+ if (!resultsVar) return;
127
+ // Remove from map so we don't reuse it
128
+ resultVarByCallIndex.delete(matchedIndex);
129
+
130
+ const resultAccess = t.memberExpression(
131
+ t.identifier(resultsVar),
132
+ t.identifier(indexVar),
133
+ true
134
+ );
135
+
136
+ awaitPath.replaceWith(resultAccess);
137
+ replacementCount++;
138
+ },
139
+ noScope: true,
140
+ });
141
+
142
+ if (replacementCount === 0) {
143
+ return false;
144
+ }
145
+
146
+ let reconstructBody: t.BlockStatement | t.Expression;
147
+ if (t.isBlockStatement(clonedBody)) {
148
+ reconstructBody = clonedBody;
149
+ } else {
150
+ reconstructBody = clonedBody as t.Expression;
151
+ }
152
+
153
+ const reconstructMapper = t.arrowFunctionExpression(
154
+ [t.identifier(param), t.identifier(indexVar)],
155
+ reconstructBody
156
+ );
157
+ reconstructMapper.async = false;
158
+
159
+ const reconstructCall = t.callExpression(
160
+ t.memberExpression(t.cloneNode(array, true), t.identifier('map')),
161
+ [reconstructMapper]
162
+ );
163
+
164
+ // Build the IIFE with all batch declarations followed by reconstruction
165
+ const iife = t.callExpression(
166
+ t.arrowFunctionExpression(
167
+ [],
168
+ t.blockStatement([...batchDeclarations, t.returnStatement(reconstructCall)]),
169
+ true // async
170
+ ),
171
+ []
172
+ );
173
+
174
+ const awaitIife = t.awaitExpression(iife);
175
+
176
+ path.replaceWith(awaitIife);
177
+ onTransform();
178
+ return true;
179
+ }
@@ -1,23 +1,168 @@
1
1
  import * as t from '@babel/types';
2
2
  import { isArrayMethod } from './utils.js';
3
+ import type { BatchParallelDetector } from './batch-detector.js';
4
+ import type { BatchCallInfo } from '../types.js';
5
+
6
+ export interface LLMCallInfo {
7
+ callNode: t.CallExpression;
8
+ callInfo: BatchCallInfo;
9
+ payloadNode: t.Expression;
10
+ // Unique key for grouping: "type:operation"
11
+ key: string;
12
+ }
3
13
 
4
14
  /**
5
- * Find LLM call expression in AST node
15
+ * Collect all identifiers referenced in an expression
6
16
  */
7
- export function findLLMCallExpression(body: t.Node): t.CallExpression | null {
8
- let found: t.CallExpression | null = null;
17
+ function collectReferencedIdentifiers(node: t.Node): Set<string> {
18
+ const identifiers = new Set<string>();
19
+
20
+ const visit = (n: t.Node) => {
21
+ if (t.isIdentifier(n)) {
22
+ identifiers.add(n.name);
23
+ }
24
+
25
+ // Continue traversing
26
+ Object.keys(n).forEach((key) => {
27
+ // Skip 'type' and other metadata fields
28
+ if (key === 'type' || key === 'loc' || key === 'start' || key === 'end') return;
29
+ const value = (n as any)[key];
30
+ if (Array.isArray(value)) {
31
+ value.forEach((item) => {
32
+ if (item && typeof item === 'object' && item.type) {
33
+ visit(item);
34
+ }
35
+ });
36
+ } else if (value && typeof value === 'object' && value.type) {
37
+ visit(value);
38
+ }
39
+ });
40
+ };
41
+
42
+ visit(node);
43
+ return identifiers;
44
+ }
45
+
46
+ /**
47
+ * Collect all variable names declared inside a callback body.
48
+ * This includes: const/let/var declarations, function parameters, etc.
49
+ */
50
+ function collectLocalVariables(body: t.Node): Set<string> {
51
+ const locals = new Set<string>();
9
52
 
10
53
  const visit = (node: t.Node) => {
11
- if (found) return;
54
+ // Variable declarations: const x = ..., let y = ..., var z = ...
55
+ if (t.isVariableDeclaration(node)) {
56
+ for (const decl of node.declarations) {
57
+ if (t.isIdentifier(decl.id)) {
58
+ locals.add(decl.id.name);
59
+ } else if (t.isObjectPattern(decl.id)) {
60
+ // Destructuring: const { a, b } = ...
61
+ for (const prop of decl.id.properties) {
62
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
63
+ locals.add(prop.value.name);
64
+ } else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
65
+ locals.add(prop.argument.name);
66
+ }
67
+ }
68
+ } else if (t.isArrayPattern(decl.id)) {
69
+ // Destructuring: const [a, b] = ...
70
+ for (const elem of decl.id.elements) {
71
+ if (t.isIdentifier(elem)) {
72
+ locals.add(elem.name);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
12
78
 
79
+ // Continue traversing (but don't descend into nested functions - their locals are their own scope)
80
+ if (t.isFunction(node)) {
81
+ return; // Don't traverse into nested functions
82
+ }
83
+
84
+ Object.keys(node).forEach((key) => {
85
+ if (key === 'type' || key === 'loc' || key === 'start' || key === 'end') return;
86
+ const value = (node as any)[key];
87
+ if (Array.isArray(value)) {
88
+ value.forEach((item) => {
89
+ if (item && typeof item === 'object' && item.type) {
90
+ visit(item);
91
+ }
92
+ });
93
+ } else if (value && typeof value === 'object' && value.type) {
94
+ visit(value);
95
+ }
96
+ });
97
+ };
98
+
99
+ visit(body);
100
+ return locals;
101
+ }
102
+
103
+ /**
104
+ * Check if LLM call payloads depend on variables that are local to the callback.
105
+ * This includes:
106
+ * - Variables computed from other expressions (const x = item.name.toUpperCase())
107
+ * - Results from previous LLM calls (const title = await atp.llm.call(...))
108
+ * - Any other locally-defined variable
109
+ *
110
+ * When batch transforming, the payload mapper only has access to the array item parameter
111
+ * and outer scope variables - NOT local variables defined inside the callback.
112
+ *
113
+ * @param body - The callback body
114
+ * @param itemParamName - The name of the array item parameter (e.g., "item")
115
+ * @param batchDetector - The batch detector for extracting LLM call info
116
+ * @returns true if there are dependencies that prevent batch transformation
117
+ */
118
+ export function hasLLMCallDependencies(
119
+ body: t.Node,
120
+ batchDetector: BatchParallelDetector,
121
+ itemParamName?: string
122
+ ): boolean {
123
+ // Find all locally-defined variables in the callback body
124
+ const localVariables = collectLocalVariables(body);
125
+
126
+ // If no local variables, no dependencies possible
127
+ if (localVariables.size === 0) {
128
+ return false;
129
+ }
130
+
131
+ // Now check if any LLM call payload references these local variables
132
+ const allCalls = findAllAwaitedMemberCalls(body);
133
+
134
+ for (const call of allCalls) {
135
+ const payloadNode = batchDetector.extractPayloadNode(call);
136
+ if (payloadNode) {
137
+ const referencedIds = collectReferencedIdentifiers(payloadNode);
138
+ // Check if any referenced identifier is a local variable
139
+ // (excluding the item parameter which is passed to the payload mapper)
140
+ for (const id of referencedIds) {
141
+ if (localVariables.has(id) && id !== itemParamName) {
142
+ return true; // Found a dependency on a local variable
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ return false;
149
+ }
150
+
151
+ /**
152
+ * Find all awaited member expression calls in AST node
153
+ */
154
+ function findAllAwaitedMemberCalls(body: t.Node): t.CallExpression[] {
155
+ const calls: t.CallExpression[] = [];
156
+
157
+ const visit = (node: t.Node) => {
13
158
  if (t.isAwaitExpression(node) && t.isCallExpression(node.argument)) {
14
159
  const call = node.argument;
15
160
  if (t.isMemberExpression(call.callee)) {
16
- found = call;
17
- return;
161
+ calls.push(call);
18
162
  }
19
163
  }
20
164
 
165
+ // Continue traversing
21
166
  Object.keys(node).forEach((key) => {
22
167
  const value = (node as any)[key];
23
168
  if (Array.isArray(value)) {
@@ -33,7 +178,42 @@ export function findLLMCallExpression(body: t.Node): t.CallExpression | null {
33
178
  };
34
179
 
35
180
  visit(body);
36
- return found;
181
+ return calls;
182
+ }
183
+
184
+ /**
185
+ * Find ALL LLM call expressions in AST node with batch info
186
+ */
187
+ export function findAllLLMCallExpressions(
188
+ body: t.Node,
189
+ batchDetector: BatchParallelDetector
190
+ ): LLMCallInfo[] {
191
+ const allCalls = findAllAwaitedMemberCalls(body);
192
+ const llmCalls: LLMCallInfo[] = [];
193
+
194
+ for (const call of allCalls) {
195
+ const callInfo = batchDetector.extractCallInfo(call);
196
+ const payloadNode = batchDetector.extractPayloadNode(call);
197
+
198
+ if (callInfo && payloadNode) {
199
+ llmCalls.push({
200
+ callNode: call,
201
+ callInfo,
202
+ payloadNode,
203
+ key: `${callInfo.type}:${callInfo.operation}`,
204
+ });
205
+ }
206
+ }
207
+
208
+ return llmCalls;
209
+ }
210
+
211
+ /**
212
+ * Find first LLM call expression in AST node
213
+ */
214
+ export function findLLMCallExpression(body: t.Node): t.CallExpression | null {
215
+ const calls = findAllAwaitedMemberCalls(body);
216
+ return calls[0] ?? null;
37
217
  }
38
218
 
39
219
  /**
@@ -1,9 +1,15 @@
1
1
  import * as t from '@babel/types';
2
2
  import { BatchOptimizer } from './batch-optimizer.js';
3
3
  import { BatchParallelDetector } from './batch-detector.js';
4
- import { getArrayMethodName, canUseBatchParallel } from './array-transformer-utils.js';
4
+ import {
5
+ getArrayMethodName,
6
+ canUseBatchParallel,
7
+ findLLMCallExpression,
8
+ hasLLMCallDependencies,
9
+ } from './array-transformer-utils.js';
5
10
  import { transformToBatchParallel } from './array-transformer-batch.js';
6
11
  import { transformToSequential } from './array-transformer-sequential.js';
12
+ import { transformToBatchWithReconstruction } from './array-transformer-batch-reconstruct.js';
7
13
 
8
14
  export class ArrayTransformer {
9
15
  private transformCount = 0;
@@ -31,6 +37,43 @@ export class ArrayTransformer {
31
37
  }
32
38
 
33
39
  const batchResult = this.batchOptimizer.canBatchArrayMethod(callback);
40
+ if (!batchResult.canBatch && methodName === 'map') {
41
+ const reason = batchResult.reason || '';
42
+ // Try batch-with-reconstruction for:
43
+ // 1. Object/array returns (would lose structure with simple batch)
44
+ // 2. Multiple pausable calls (can batch each call type separately)
45
+ const canTryBatchReconstruct =
46
+ reason.includes('object expression') ||
47
+ reason.includes('array expression') ||
48
+ reason.includes('Multiple pausable calls');
49
+
50
+ if (canTryBatchReconstruct) {
51
+ const llmCall = findLLMCallExpression(callback.body);
52
+ // Get the item parameter name (e.g., "item" in items.map(async (item) => ...))
53
+ const itemParam = callback.params[0];
54
+ const itemParamName = t.isIdentifier(itemParam) ? itemParam.name : undefined;
55
+ // Check for dependencies on local variables (e.g., computed values, previous LLM results)
56
+ // If dependencies exist, we can't batch because payload mapper only has access to item + outer scope
57
+ const hasDependencies = hasLLMCallDependencies(
58
+ callback.body,
59
+ this.batchDetector,
60
+ itemParamName
61
+ );
62
+ if (llmCall && !hasDependencies) {
63
+ const success = transformToBatchWithReconstruction(
64
+ path,
65
+ node,
66
+ methodName,
67
+ callback,
68
+ this.batchDetector,
69
+ () => this.transformCount++
70
+ );
71
+ if (success) {
72
+ return true;
73
+ }
74
+ }
75
+ }
76
+ }
34
77
 
35
78
  if (batchResult.canBatch && canUseBatchParallel(methodName)) {
36
79
  const array = (node.callee as t.MemberExpression).object;
@@ -171,5 +171,6 @@ export * from './batch-detector.js';
171
171
  export * from './batch-optimizer.js';
172
172
  export * from './loop-transformer.js';
173
173
  export * from './array-transformer.js';
174
+ export * from './array-transformer-batch-reconstruct.js';
174
175
  export * from './promise-transformer.js';
175
176
  export * from './utils.js';