relationalai 0.11.2__py3-none-any.whl → 0.11.4__py3-none-any.whl

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.
Files changed (42) hide show
  1. relationalai/clients/snowflake.py +44 -15
  2. relationalai/clients/types.py +1 -0
  3. relationalai/clients/use_index_poller.py +446 -178
  4. relationalai/early_access/builder/std/__init__.py +1 -1
  5. relationalai/early_access/dsl/bindings/csv.py +4 -4
  6. relationalai/semantics/internal/internal.py +22 -4
  7. relationalai/semantics/lqp/executor.py +69 -18
  8. relationalai/semantics/lqp/intrinsics.py +23 -0
  9. relationalai/semantics/lqp/model2lqp.py +16 -6
  10. relationalai/semantics/lqp/passes.py +3 -4
  11. relationalai/semantics/lqp/primitives.py +38 -14
  12. relationalai/semantics/metamodel/builtins.py +152 -11
  13. relationalai/semantics/metamodel/factory.py +3 -2
  14. relationalai/semantics/metamodel/helpers.py +78 -2
  15. relationalai/semantics/reasoners/graph/core.py +343 -40
  16. relationalai/semantics/reasoners/optimization/solvers_dev.py +20 -1
  17. relationalai/semantics/reasoners/optimization/solvers_pb.py +24 -3
  18. relationalai/semantics/rel/compiler.py +5 -17
  19. relationalai/semantics/rel/executor.py +2 -2
  20. relationalai/semantics/rel/rel.py +6 -0
  21. relationalai/semantics/rel/rel_utils.py +37 -1
  22. relationalai/semantics/rel/rewrite/extract_common.py +153 -242
  23. relationalai/semantics/sql/compiler.py +540 -202
  24. relationalai/semantics/sql/executor/duck_db.py +21 -0
  25. relationalai/semantics/sql/executor/result_helpers.py +7 -0
  26. relationalai/semantics/sql/executor/snowflake.py +9 -2
  27. relationalai/semantics/sql/rewrite/denormalize.py +4 -6
  28. relationalai/semantics/sql/rewrite/recursive_union.py +23 -3
  29. relationalai/semantics/sql/sql.py +120 -46
  30. relationalai/semantics/std/__init__.py +9 -4
  31. relationalai/semantics/std/datetime.py +363 -0
  32. relationalai/semantics/std/math.py +77 -0
  33. relationalai/semantics/std/re.py +83 -0
  34. relationalai/semantics/std/strings.py +1 -1
  35. relationalai/tools/cli_controls.py +445 -60
  36. relationalai/util/format.py +78 -1
  37. {relationalai-0.11.2.dist-info → relationalai-0.11.4.dist-info}/METADATA +3 -2
  38. {relationalai-0.11.2.dist-info → relationalai-0.11.4.dist-info}/RECORD +41 -39
  39. relationalai/semantics/std/dates.py +0 -213
  40. {relationalai-0.11.2.dist-info → relationalai-0.11.4.dist-info}/WHEEL +0 -0
  41. {relationalai-0.11.2.dist-info → relationalai-0.11.4.dist-info}/entry_points.txt +0 -0
  42. {relationalai-0.11.2.dist-info → relationalai-0.11.4.dist-info}/licenses/LICENSE +0 -0
@@ -7,6 +7,7 @@ from relationalai.semantics.metamodel import ir, factory as f, helpers
7
7
  from relationalai.semantics.metamodel.compiler import Pass, group_tasks
8
8
  from relationalai.semantics.metamodel.util import OrderedSet, ordered_set
9
9
  from relationalai.semantics.metamodel import dependency
10
+ from relationalai.semantics.metamodel import builtins
10
11
 
11
12
  class ExtractCommon(Pass):
12
13
  """
@@ -35,6 +36,17 @@ class ExtractCommon(Pass):
35
36
  Logical2 ...
36
37
  """
37
38
 
39
+ # The extraction plan heuristic is as follows:
40
+ #
41
+ # Given a set of binder tasks B and a set of extractable tasks E, we find:
42
+ # - A subset of common tasks C in B, and
43
+ # - A subset of exposed variables V output from tasks in C
44
+ # where:
45
+ # - The intersection of common dependencies of all tasks in E are contained in C
46
+ # (including transitive dependencies)
47
+ # - The union of input variables for all tasks in E intersected with the output
48
+ # variables of tasks in C are contained in V
49
+
38
50
  #--------------------------------------------------
39
51
  # Public API
40
52
  #--------------------------------------------------
@@ -77,112 +89,124 @@ class ExtractCommon(Pass):
77
89
  return task
78
90
 
79
91
  def handle_logical(self, task: ir.Logical, ctx: Context):
80
- # create frame to process the children
81
- # with ctx_frame(ctx, task) as frame:
82
- # process the original body
83
- groups = group_tasks(task.body, {
84
- "binders": helpers.BINDERS,
85
- "composites": helpers.COMPOSITES
86
- })
87
- binders = groups["binders"]
88
- composites = groups["composites"]
89
-
90
- # the new body of the rewritten task
91
- body:OrderedSet[ir.Task] = ordered_set()
92
-
93
- # quick check to see if it's worth doing more analysis; we only want to extract
94
- # common binders if there are multiple, and there are also multiple composites
95
- # that will be extracted by the flatten pass later (so that they can share the
96
- # extracted logic).
97
- plan = None
98
- if len(binders) > 1 and composites:
99
- extractables = flatten.extractables(composites)
100
- # only makes sense to extract common if at least one nested composite will be extracted
101
- if extractables:
102
- # make a plan to extract common tasks from the logical
103
- plan = self._create_extraction_plan(binders, composites, extractables, ctx)
104
- if plan and len(composites) > 1:
105
- # plan is worthwhile and there are multiple composites, extract the common body and add the connection to the body
106
- exposed_vars = plan.exposed_vars.get_list()
107
- plan.common_reference = f.lookup(helpers.extract(task, plan.common_body, exposed_vars, ctx.rewrite_ctx, "common"), exposed_vars)
108
- # if we are not distributing the reference, add to the main body
109
- if not plan.distribute_common_reference:
110
- body.add(plan.common_reference)
111
-
112
- # if we have a plan and will distribute the common reference, keep track of
113
- # variables still needed by the remaining tasks, as they need to be hoisted by
114
- # the remaining composites that get the common reference
115
- remaining_vars = None
116
- if plan and plan.distribute_common_reference:
117
- # add variables hoisted by this logical that are in the exposed vars, to
118
- # make sure they are hoisted all the way through
119
- remaining_vars = OrderedSet.from_iterable(helpers.hoisted_vars(task.hoisted)) & plan.exposed_vars
120
- for child in task.body:
121
- if child in groups["other"] or child not in plan.remaining_body or child in composites:
122
- continue
123
- remaining_vars.update(ctx.info.task_inputs(child))
124
- remaining_vars.update(ctx.info.task_outputs(child))
125
- remaining_vars = remaining_vars & plan.exposed_vars
126
-
127
- # if the plan was not used in one of the cases above, ignore it completely, we
128
- # are not extracting common nor distributing it around
129
- if plan and not plan.distribute_common_reference and not len(composites) > 1:
130
- plan = None
131
-
132
- # recursively handle children
92
+ # Process the original body to find binders and extractables. The Flatten pass later
93
+ # will extract out both composites and effects, so we group them together here
94
+ groups = group_tasks(task.body, {
95
+ "binders": helpers.BINDERS,
96
+ "composites_and_effects": helpers.COMPOSITES + helpers.EFFECTS,
97
+ })
98
+ binders = groups["binders"]
99
+ composites_and_effects = groups["composites_and_effects"]
100
+
101
+ # the new body of the rewritten task
102
+ body:OrderedSet[ir.Task] = ordered_set()
103
+
104
+ # quick check to see if it's worth doing more analysis; we only want to extract
105
+ # common binders if there are multiple, and there are also multiple composites
106
+ # that will be extracted by the flatten pass later (so that they can share the
107
+ # extracted logic).
108
+ plan = None
109
+ if len(binders) > 1 and composites_and_effects:
110
+ extractables = flatten.extractables(composites_and_effects)
111
+ # only makes sense to extract common if at least one nested composite will be
112
+ # extracted during Flatten
113
+ if extractables:
114
+ # make a plan to extract common tasks from the logical
115
+ plan = self._create_extraction_plan(binders, composites_and_effects, extractables, ctx)
116
+ if plan and len(composites_and_effects) > 1:
117
+ # plan is worthwhile and there are multiple composites, extract the common body and add the connection to the body
118
+ exposed_vars = plan.exposed_vars.get_list()
119
+ plan.common_reference = f.lookup(helpers.extract(task, plan.common_body, exposed_vars, ctx.rewrite_ctx, "common"), exposed_vars)
120
+ # if we are not distributing the reference, add to the main body
121
+ if not plan.distribute_common_reference:
122
+ body.add(plan.common_reference)
123
+
124
+ # if we have a plan and will distribute the common reference, keep track of
125
+ # variables still needed by the remaining tasks, as they need to be hoisted by
126
+ # the remaining composites that get the common reference
127
+ remaining_vars = None
128
+ if plan and plan.distribute_common_reference:
129
+ # add variables hoisted by this logical that are in the exposed vars, to
130
+ # make sure they are hoisted all the way through
131
+ remaining_vars = OrderedSet.from_iterable(helpers.hoisted_vars(task.hoisted)) & plan.exposed_vars
133
132
  for child in task.body:
134
- # skip children that were extracted
135
- if plan and child not in groups["other"] and child not in plan.remaining_body and child not in composites:
133
+ if child in groups["other"] or child not in plan.remaining_body or child in composites_and_effects:
136
134
  continue
135
+ remaining_vars.update(ctx.info.task_inputs(child))
136
+ remaining_vars.update(ctx.info.task_outputs(child))
137
+ remaining_vars = remaining_vars & plan.exposed_vars
137
138
 
138
- # no plan or child is not a composite, so just add the handled to the body
139
- if not plan or child not in composites:
140
- body.add(self.handle(child, ctx))
141
- continue
139
+ # if the plan was not used in one of the cases above, ignore it completely, we
140
+ # are not extracting common nor distributing it around
141
+ if plan and not plan.distribute_common_reference and not len(composites_and_effects) > 1:
142
+ plan = None
143
+
144
+ # recursively handle children
145
+ for child in task.body:
146
+ # skip children that were extracted
147
+ if plan and child not in groups["other"] and child not in plan.remaining_body and child not in composites_and_effects:
148
+ continue
149
+
150
+ # no plan or child is not a composite, so just add the handled to the body
151
+ if not plan or child not in composites_and_effects:
152
+ body.add(self.handle(child, ctx))
153
+ continue
142
154
 
143
- # there is a plan and the child is in composites, so...
144
- replacement = self.handle(child, ctx)
145
-
146
- # this child needs either extra local dependencies or the common reference
147
- if child in plan.local_dependencies or plan.distribute_common_reference:
148
- # the new body will have maybe the common reference and the local deps
149
- replacement_body = ordered_set()
150
- hoisted = OrderedSet.from_iterable(replacement.hoisted if isinstance(replacement, helpers.COMPOSITES) else [])
151
- if plan.distribute_common_reference:
152
- if len(composites) == 1:
153
- # if there's a single composite, just insert the whole common body into it
154
- replacement_body.update(plan.common_body)
155
- else:
156
- # otherwise insert a clone of the reference on the extracted rule
157
- assert(plan.common_reference)
158
- replacement_body.add(plan.common_reference.clone())
159
- # add remaining vars to hoisted, making sure there's no duplicates (due to VarOrDefault)
160
- hoisted_vars = helpers.hoisted_vars(hoisted)
161
- if remaining_vars:
162
- hoisted = OrderedSet.from_iterable(filter(lambda v: v not in hoisted_vars, remaining_vars)) | hoisted
163
-
164
- if child in plan.local_dependencies:
165
- for local_dep in plan.local_dependencies[child]:
166
- replacement_body.add(local_dep.clone())
167
- #replacement_body.update(plan.local_dependencies[child])
168
-
169
- if isinstance(replacement, ir.Logical):
170
- # if the replacements is a logical, we can just add to the body
171
- body.add(replacement.reconstruct(
172
- replacement.engine,
173
- tuple(hoisted.get_list()),
174
- tuple(replacement_body.update(replacement.body).get_list()),
175
- replacement.annotations
176
- ))
155
+ # there is a plan and the child is in composites, so...
156
+ replacement = self.handle(child, ctx)
157
+
158
+ # this child needs either extra local dependencies or the common reference
159
+ if child in plan.local_dependencies or plan.distribute_common_reference:
160
+ # the new body will have maybe the common reference and the local deps
161
+ replacement_body = ordered_set()
162
+
163
+ hoisted = OrderedSet()
164
+ if isinstance(replacement, ir.Logical):
165
+ # if replacement is a logical, just keep the same hoisted vars
166
+ hoisted.update(replacement.hoisted)
167
+ else:
168
+ # otherwise, we need to hoist the vars that are output from local deps
169
+ # and input to the replacement task
170
+ dep_outputs = OrderedSet()
171
+ for d in plan.local_dependencies.get(child, ordered_set()):
172
+ dep_outputs.update(ctx.info.task_outputs(d))
173
+ hoisted.update(dep_outputs & ctx.info.task_inputs(replacement))
174
+
175
+ if plan.distribute_common_reference:
176
+ if len(composites_and_effects) == 1:
177
+ # if there's a single composite, just insert the whole common body into it
178
+ replacement_body.update(plan.common_body)
177
179
  else:
178
- # anything else is now wrapped in a logical so that we can add the
179
- # local dependencies
180
- body.add(f.logical(replacement_body.add(replacement).get_list(), hoisted.get_list(), replacement.engine))
180
+ # otherwise insert a clone of the reference on the extracted rule
181
+ assert(plan.common_reference)
182
+ replacement_body.add(plan.common_reference.clone())
183
+ # add remaining vars to hoisted, making sure there's no duplicates (due to VarOrDefault)
184
+ hoisted_vars = helpers.hoisted_vars(hoisted)
185
+ if remaining_vars:
186
+ hoisted = OrderedSet.from_iterable(filter(lambda v: v not in hoisted_vars, remaining_vars)) | hoisted
187
+
188
+ if child in plan.local_dependencies:
189
+ for local_dep in plan.local_dependencies[child]:
190
+ replacement_body.add(local_dep.clone())
191
+
192
+ if isinstance(replacement, ir.Logical):
193
+ # if the replacements is a logical, we can just add to the body
194
+ body.add(replacement.reconstruct(
195
+ replacement.engine,
196
+ tuple(hoisted.get_list()),
197
+ tuple(replacement_body.update(replacement.body).get_list()),
198
+ replacement.annotations
199
+ ))
181
200
  else:
182
- # child does not need extras in the body, just add it to the main body
201
+ # Otherwise, wrap the local dependencies in a Lookup where the output
202
+ # variables are hoisted, and keep the computed replacement.
203
+ body.add(f.logical(replacement_body.get_list(), hoisted.get_list(), replacement.engine))
183
204
  body.add(replacement)
205
+ else:
206
+ # child does not need extras in the body, just add it to the main body
207
+ body.add(replacement)
184
208
 
185
- return ir.Logical(task.engine, task.hoisted, tuple(body))
209
+ return ir.Logical(task.engine, task.hoisted, tuple(body))
186
210
 
187
211
  @dataclass
188
212
  class ExtractionPlan():
@@ -206,16 +230,23 @@ class ExtractCommon(Pass):
206
230
  Compute a plan to extract tasks in this frame that are common dependencies
207
231
  across these composite tasks.
208
232
  """
209
- # compute intersection of task dependencies and inputs
233
+ # If there are any pragma lookups, then don't extract anything. Pragma lookups are
234
+ # designed to control execution order, and extracting them may affect their
235
+ # semantics.
236
+ for b in binders:
237
+ if isinstance(b, ir.Lookup) and builtins.is_pragma(b.relation):
238
+ return None
239
+
240
+ # Compute intersection of task dependencies
210
241
  sample = composites.some()
211
242
  deps = ctx.info.task_dependencies(sample)
212
243
  if deps is None:
213
244
  return None
214
245
  # only get sibling dependencies
215
246
  common_body = binders & deps
216
- exposed_vars = OrderedSet.from_iterable(ctx.info.task_inputs(sample))
217
247
 
218
- # now extract from the original sets
248
+ # For other composites, remove their sibling dependencies so that we end up with
249
+ # the intersection of dependencies
219
250
  for composite in composites:
220
251
  if composite is sample:
221
252
  continue
@@ -226,11 +257,23 @@ class ExtractCommon(Pass):
226
257
  for task in common_body:
227
258
  if task not in deps:
228
259
  common_body.remove(task)
260
+
261
+ # Compute union of input vars
262
+ # Start with the output vars of the common body. We only want to expose vars that
263
+ # are output from the common body
264
+ body_output_vars = OrderedSet()
265
+ for child in common_body:
266
+ body_output_vars.update(ctx.info.task_outputs(child))
267
+
268
+ # Compute the union of input vars across all composites, intersected with output
269
+ # vars of the common body
270
+ exposed_vars = OrderedSet.from_iterable(ctx.info.task_inputs(sample)) & body_output_vars
271
+ for composite in composites:
272
+ if composite is sample:
273
+ continue
229
274
  # compute common input vars
230
- t_inputs = ctx.info.task_inputs(composite)
231
- for v in exposed_vars:
232
- if v not in t_inputs:
233
- exposed_vars.remove(v)
275
+ t_inputs = OrderedSet.from_iterable(ctx.info.task_inputs(composite))
276
+ exposed_vars.update(t_inputs & body_output_vars)
234
277
 
235
278
  # no vars in common, not worth to extract
236
279
  if not exposed_vars:
@@ -263,7 +306,7 @@ class ExtractCommon(Pass):
263
306
  for binder in binders:
264
307
  if binder not in common_body:
265
308
  remaining.add(binder)
266
- deps = self._compute_local_dependencies(ctx, binders, binder, common_body, exposed_vars)
309
+ deps = self._compute_local_dependencies(ctx, binders, binder, exposed_vars)
267
310
  if deps:
268
311
  remaining.update(deps)
269
312
 
@@ -271,16 +314,17 @@ class ExtractCommon(Pass):
271
314
  # depends on it but it is not exposed by the vars
272
315
  local_dependencies: dict[ir.Task, OrderedSet[ir.Task]] = dict()
273
316
  for composite in composites:
274
- local = self._compute_local_dependencies(ctx, binders, composite, common_body, exposed_vars)
317
+ local = self._compute_local_dependencies(ctx, binders, composite, exposed_vars)
275
318
  if local:
276
319
  local_dependencies[composite] = local
277
320
 
278
321
  # distribute the common reference only if all of the composites are extractable and there's nothing else remaining
279
322
  distribute_common_reference = len(extractables) == len(composites) and not remaining
323
+
280
324
  return ExtractCommon.ExtractionPlan(common_body, remaining, exposed_vars, local_dependencies, distribute_common_reference)
281
325
 
282
326
 
283
- def _compute_local_dependencies(self, ctx: Context, binders: OrderedSet[ir.Task], composite: ir.Task, common_body: OrderedSet[ir.Task], exposed_vars: OrderedSet[ir.Var]):
327
+ def _compute_local_dependencies(self, ctx: Context, binders: OrderedSet[ir.Task], composite: ir.Task, exposed_vars: OrderedSet[ir.Var]):
284
328
  """
285
329
  The tasks in common_body will be extracted into a logical that will expose the exposed_vars.
286
330
  Compute which additional dependencies are needed specifically for this composite, because
@@ -298,145 +342,12 @@ class ExtractCommon(Pass):
298
342
  if not vars_needed:
299
343
  return None
300
344
 
301
- # needs = ctx.info.local_dependencies(composite) & binders | common_body
302
- # if needs:
303
- # return needs
304
- # return None
305
-
306
- # this is a greedy algorithm that uses the first task in the common body that provides
307
- # a variable needed; it may result in sub-optimal extraction, but should be correct
308
- local_body = ordered_set()
309
- while(vars_needed):
310
- v = vars_needed.pop()
311
- for x in common_body:
312
- if x not in local_body:
313
- # an x that is not yet in local_body can fulfill v
314
- x_outputs = ctx.info.task_outputs(x)
315
- if x_outputs and v in x_outputs:
316
- # add it to local_body and add its outputs to vars exposed
317
- local_body.add(x)
318
- vars_exposed.add(x_outputs)
319
- # but add its inputs the vars now needed
320
- inputs = ctx.info.task_inputs(x)
321
- if inputs:
322
- vars_needed.update(inputs - vars_exposed)
323
- return local_body
324
-
325
-
326
-
327
-
328
-
329
- def _create_extraction_plan_old(self, binders: OrderedSet[ir.Task], composites: OrderedSet[ir.Task], extractables: list[ir.Task], ctx: Context):
330
- """
331
- Compute a plan to extract tasks in this frame that are common dependencies
332
- across these composite tasks.
333
- """
334
- # compute intersection of task dependencies and inputs
335
- sample = composites.some()
336
- deps = ctx.info.task_dependencies(sample)
337
- if deps is None:
338
- return None
339
- # only get sibling dependencies
340
- common_body = binders & deps
341
- exposed_vars = OrderedSet.from_iterable(ctx.info.task_inputs(sample))
342
-
343
- # now extract from the original sets
344
- for composite in composites:
345
- if composite is sample:
346
- continue
347
-
348
- # compute sibling dependencies
349
- deps = ctx.info.task_dependencies(composite)
350
- if deps:
351
- for task in common_body:
352
- if task not in deps:
353
- common_body.remove(task)
354
- # compute common input vars
355
- t_inputs = ctx.info.task_inputs(composite)
356
- for v in exposed_vars:
357
- if v not in t_inputs:
358
- exposed_vars.remove(v)
359
-
360
- # no vars in common, not worth to extract
361
- if not exposed_vars:
362
- return None
363
-
364
- # pull the transitive closure of the intersected tasks in the common body
365
- chasing = True
366
- while(chasing):
367
- chasing = False
368
- for task in common_body:
369
- deps = ctx.info.task_dependencies(task)
370
- if deps:
371
- for hop in binders & deps:
372
- if hop not in common_body:
373
- common_body.add(hop)
374
- chasing = True
375
-
376
- # not useful to extract common tasks if there's a single one
377
- if len(common_body) < 2:
378
- return None
379
-
380
- # check if some variable used in the common body is needed by some binder that is
381
- # not going to be extracted. In that case, we need to expose this variable from the
382
- # common body
383
- common_vars = ordered_set()
384
- for task in common_body:
385
- common_vars.update(ctx.info.task_outputs(task))
386
- common_vars = common_vars - exposed_vars
387
- for v in common_vars:
388
- for binder in binders:
389
- if binder not in common_body and ctx.info.task_inputs(binder) and v in ctx.info.task_inputs(binder):
390
- exposed_vars.add(v)
391
- break
392
-
393
- # check with of the original binders remain, and make sure their dependencies also stay
394
- remaining = ordered_set()
395
- for binder in binders:
396
- if binder not in common_body:
397
- remaining.add(binder)
398
- deps = self._compute_local_dependencies_old(ctx, binder, common_body, exposed_vars)
399
- if deps:
400
- remaining.update(deps)
401
-
402
- # for each composite, check if there are additional tasks needed, because the task
403
- # depends on it but it is not exposed by the vars
404
- local_dependencies: dict[ir.Task, OrderedSet[ir.Task]] = dict()
405
- for composite in composites:
406
- local = self._compute_local_dependencies_old(ctx, composite, common_body, exposed_vars)
407
- if local:
408
- local_dependencies[composite] = local
409
-
410
- # distribute the common reference only if all of the composites are extractable and there's nothing else remaining
411
- distribute_common_reference = len(extractables) == len(composites) and not remaining
412
- # TODO: we are forcing this for now, but we should remove the whole analysis
413
- distribute_common_reference = False
414
- return ExtractCommon.ExtractionPlan(common_body, remaining, exposed_vars, local_dependencies, distribute_common_reference)
415
-
416
-
417
- def _compute_local_dependencies_old(self, ctx: Context, composite: ir.Task, common_body: OrderedSet[ir.Task], exposed_vars: OrderedSet[ir.Var]):
418
- """
419
- The tasks in common_body will be extracted into a logical that will expose the exposed_vars.
420
- Compute which additional dependencies are needed specifically to this composite, because
421
- it depends on some tasks that are extracted to common_body but not exposed by exposed_vars.
422
- """
423
-
424
- # vars exposed by exposed vars + tasks added to the local body
425
- vars_exposed = OrderedSet.from_iterable(exposed_vars)
426
- # working list of vars we still need to fulfill
427
- inputs = ctx.info.task_inputs(composite)
428
- if not inputs:
429
- return None
430
- vars_needed = (inputs - vars_exposed)
431
- if not vars_needed:
432
- return None
433
-
434
345
  # this is a greedy algorithm that uses the first task in the common body that provides
435
346
  # a variable needed; it may result in sub-optimal extraction, but should be correct
436
347
  local_body = ordered_set()
437
348
  while(vars_needed):
438
349
  v = vars_needed.pop()
439
- for x in common_body:
350
+ for x in binders:
440
351
  if x not in local_body:
441
352
  # an x that is not yet in local_body can fulfill v
442
353
  x_outputs = ctx.info.task_outputs(x)