relationalai 0.13.2__py3-none-any.whl → 0.13.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 (52) hide show
  1. relationalai/clients/client.py +3 -4
  2. relationalai/clients/exec_txn_poller.py +62 -31
  3. relationalai/clients/resources/snowflake/direct_access_resources.py +6 -5
  4. relationalai/clients/resources/snowflake/snowflake.py +54 -51
  5. relationalai/clients/resources/snowflake/use_index_poller.py +1 -1
  6. relationalai/semantics/internal/snowflake.py +5 -1
  7. relationalai/semantics/lqp/algorithms.py +173 -0
  8. relationalai/semantics/lqp/builtins.py +199 -2
  9. relationalai/semantics/lqp/executor.py +90 -41
  10. relationalai/semantics/lqp/export_rewriter.py +40 -0
  11. relationalai/semantics/lqp/ir.py +28 -2
  12. relationalai/semantics/lqp/model2lqp.py +218 -45
  13. relationalai/semantics/lqp/passes.py +13 -658
  14. relationalai/semantics/lqp/rewrite/__init__.py +12 -0
  15. relationalai/semantics/lqp/rewrite/algorithm.py +385 -0
  16. relationalai/semantics/lqp/rewrite/annotate_constraints.py +22 -10
  17. relationalai/semantics/lqp/rewrite/constants_to_vars.py +70 -0
  18. relationalai/semantics/lqp/rewrite/deduplicate_vars.py +104 -0
  19. relationalai/semantics/lqp/rewrite/eliminate_data.py +108 -0
  20. relationalai/semantics/lqp/rewrite/functional_dependencies.py +31 -2
  21. relationalai/semantics/lqp/rewrite/period_math.py +77 -0
  22. relationalai/semantics/lqp/rewrite/quantify_vars.py +65 -31
  23. relationalai/semantics/lqp/rewrite/unify_definitions.py +317 -0
  24. relationalai/semantics/lqp/utils.py +11 -1
  25. relationalai/semantics/lqp/validators.py +14 -1
  26. relationalai/semantics/metamodel/builtins.py +2 -1
  27. relationalai/semantics/metamodel/compiler.py +2 -1
  28. relationalai/semantics/metamodel/dependency.py +12 -3
  29. relationalai/semantics/metamodel/executor.py +11 -1
  30. relationalai/semantics/metamodel/factory.py +2 -2
  31. relationalai/semantics/metamodel/helpers.py +7 -0
  32. relationalai/semantics/metamodel/ir.py +3 -2
  33. relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +30 -20
  34. relationalai/semantics/metamodel/rewrite/flatten.py +50 -13
  35. relationalai/semantics/metamodel/rewrite/format_outputs.py +9 -3
  36. relationalai/semantics/metamodel/typer/checker.py +6 -4
  37. relationalai/semantics/metamodel/typer/typer.py +2 -5
  38. relationalai/semantics/metamodel/visitor.py +4 -3
  39. relationalai/semantics/reasoners/optimization/solvers_dev.py +1 -1
  40. relationalai/semantics/reasoners/optimization/solvers_pb.py +3 -4
  41. relationalai/semantics/rel/compiler.py +2 -1
  42. relationalai/semantics/rel/executor.py +3 -2
  43. relationalai/semantics/tests/lqp/__init__.py +0 -0
  44. relationalai/semantics/tests/lqp/algorithms.py +345 -0
  45. relationalai/semantics/tests/test_snapshot_abstract.py +2 -1
  46. relationalai/tools/cli_controls.py +216 -67
  47. relationalai/util/format.py +5 -2
  48. {relationalai-0.13.2.dist-info → relationalai-0.13.4.dist-info}/METADATA +2 -2
  49. {relationalai-0.13.2.dist-info → relationalai-0.13.4.dist-info}/RECORD +52 -42
  50. {relationalai-0.13.2.dist-info → relationalai-0.13.4.dist-info}/WHEEL +0 -0
  51. {relationalai-0.13.2.dist-info → relationalai-0.13.4.dist-info}/entry_points.txt +0 -0
  52. {relationalai-0.13.2.dist-info → relationalai-0.13.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,22 +1,31 @@
1
1
  from relationalai.semantics.metamodel import ir, builtins, helpers, types
2
2
  from relationalai.semantics.metamodel.visitor import collect_by_type
3
- from relationalai.semantics.metamodel.util import FrozenOrderedSet
4
- from relationalai.semantics.lqp import ir as lqp, utils, types as lqp_types, builtins as lqp_builtins
3
+ from relationalai.semantics.metamodel.util import FrozenOrderedSet, OrderedSet
4
+ from relationalai.semantics.metamodel.compiler import group_tasks
5
+ from relationalai.semantics.lqp import ir as lqp, utils, types as lqp_types
5
6
  from relationalai.semantics.lqp.primitives import lqp_avg_op, lqp_operator, build_primitive
6
7
  from relationalai.semantics.lqp.pragmas import pragma_to_lqp_name
7
8
  from relationalai.semantics.lqp.types import meta_type_to_lqp
8
9
  from relationalai.semantics.lqp.constructors import (
9
10
  mk_abstraction, mk_and, mk_exists, mk_or, mk_pragma, mk_primitive,
10
- mk_specialized_value, mk_type, mk_value, mk_attribute
11
+ mk_specialized_value, mk_type, mk_value, mk_attribute,
11
12
  )
12
- from relationalai.semantics.lqp.utils import TranslationCtx, gen_unique_var
13
+ from relationalai.semantics.lqp.algorithms import (
14
+ is_script, is_algorithm_script, is_algorithm_logical,
15
+ is_while_loop, is_while_script, construct_monoid, is_empty_instruction
16
+ )
17
+ from relationalai.semantics.lqp.builtins import (
18
+ has_global_annotation, get_upsert_annotation, get_monoid_annotation,
19
+ get_monus_annotation, has_assign_annotation, get_arity, supported_lqp_annotations
20
+ )
21
+ from relationalai.semantics.lqp.utils import TranslationCtx, ExportDescriptor, gen_unique_var, gen_rel_id
13
22
  from relationalai.semantics.lqp.validators import assert_valid_input
14
23
  from relationalai.semantics.lqp.rewrite.functional_dependencies import (
15
24
  normalized_fd, contains_only_declarable_constraints
16
25
  )
17
26
  from decimal import Decimal as PyDecimal
18
27
  from datetime import datetime, date, timezone
19
- from typing import Tuple, cast, Union, Optional
28
+ from typing import Sequence, Tuple, cast, Union, Optional
20
29
  from warnings import warn
21
30
  import re
22
31
  import uuid
@@ -42,8 +51,8 @@ def to_lqp(model: ir.Model, fragment_name: bytes, ctx: TranslationCtx) -> tuple[
42
51
  reads.extend(_get_output_reads(ctx.output_ids))
43
52
 
44
53
  export_info = None
45
- if len(ctx.export_ids) > 0:
46
- export_filename, col_types, export_reads = _get_export_reads(ctx.export_ids)
54
+ if len(ctx.export_descriptors) > 0:
55
+ export_filename, col_types, export_reads = _get_export_reads(ctx.export_descriptors)
47
56
  reads.extend(export_reads)
48
57
  export_info = (export_filename, col_types)
49
58
 
@@ -76,14 +85,17 @@ def _get_output_reads(output_ids: list[tuple[lqp.RelationId, str]]) -> list[lqp.
76
85
  reads.append(lqp.Read(read_type=output, meta=None))
77
86
  return reads
78
87
 
79
- def _get_export_reads(export_ids: list[tuple[lqp.RelationId, int, lqp.Type]]) -> tuple[str, list, list[lqp.Read]]:
88
+ def _get_export_reads(descriptors: list[ExportDescriptor]) -> tuple[str, list, list[lqp.Read]]:
80
89
  reads = []
81
90
  csv_columns = []
82
91
  col_info = []
83
- for (col_id, col_num, col_type) in sorted(export_ids, key=lambda x: x[1]):
84
- col_name = f"col{col_num:03}"
85
- csv_columns.append(lqp.ExportCSVColumn(column_name=col_name, column_data=col_id, meta=None))
86
- col_info.append((col_name, col_type))
92
+ for descriptor in sorted(descriptors, key=lambda x: x.column_number):
93
+ csv_columns.append(lqp.ExportCSVColumn(
94
+ column_name=descriptor.column_name,
95
+ column_data=descriptor.relation_id,
96
+ meta=None,
97
+ ))
98
+ col_info.append((descriptor.column_name, descriptor.column_type))
87
99
 
88
100
  # Generate a random name for the internal export path
89
101
  export_filename = "export_" + str(uuid.uuid4()).replace("-", "_")
@@ -106,6 +118,8 @@ def _get_export_reads(export_ids: list[tuple[lqp.RelationId, int, lqp.Type]]) ->
106
118
  def _translate_to_decls(ctx: TranslationCtx, rule: ir.Logical) -> list[lqp.Declaration]:
107
119
  if contains_only_declarable_constraints(rule):
108
120
  return _translate_to_constraint_decls(ctx, rule)
121
+ elif is_algorithm_logical(rule):
122
+ return _translate_algorithms(ctx, rule)
109
123
  else:
110
124
  return _translate_to_standard_decl(ctx, rule)
111
125
 
@@ -128,8 +142,11 @@ def _translate_to_constraint_decls(ctx: TranslationCtx, rule: ir.Logical) -> lis
128
142
  lqp_guard = mk_abstraction(lqp_typed_vars, mk_and(lqp_guard_atoms))
129
143
  lqp_keys:list[lqp.Var] = [var for (var, _) in lqp_typed_keys] # type: ignore
130
144
  lqp_values:list[lqp.Var] = [var for (var, _) in lqp_typed_values] # type: ignore
145
+ lqp_id = utils.lqp_hash(fd.canonical_str)
146
+ lqp_name:lqp.RelationId = lqp.RelationId(id=lqp_id, meta=None)
131
147
 
132
148
  fd_decl = lqp.FunctionalDependency(
149
+ name=lqp_name,
133
150
  guard=lqp_guard,
134
151
  keys=lqp_keys,
135
152
  values=lqp_values,
@@ -140,39 +157,161 @@ def _translate_to_constraint_decls(ctx: TranslationCtx, rule: ir.Logical) -> lis
140
157
 
141
158
  return constraint_decls
142
159
 
143
- def _translate_to_standard_decl(ctx: TranslationCtx, rule: ir.Logical) -> list[lqp.Declaration]:
144
- effects = collect_by_type((ir.Output, ir.Update), rule)
145
- aggregates = collect_by_type(ir.Aggregate, rule)
146
- ranks = collect_by_type(ir.Rank, rule)
160
+ def _translate_algorithms(ctx: TranslationCtx, task: ir.Logical) -> list[lqp.Declaration]:
161
+ assert is_algorithm_logical(task)
162
+ decls: list[lqp.Declaration] = []
163
+ for subtask in task.body:
164
+ assert is_algorithm_script(subtask), "Expected all subtasks to be algorithm scripts"
165
+ decls.extend(_translate_algorithm_script(ctx, subtask))
166
+ return decls
167
+
168
+ def _translate_algorithm_script(ctx: TranslationCtx, alg_task: ir.Sequence) -> list[lqp.Declaration]:
169
+ assert is_algorithm_script(alg_task), "Expected Sequence @algorithm @script "
170
+
171
+ alg_globals = _find_algorithm_global_relation_ids(ctx, alg_task)
172
+ alg_body = _translate_script(ctx, alg_task)
173
+
174
+ return [lqp.Algorithm(global_=alg_globals, body=alg_body, meta=None)]
175
+
176
+ def _find_algorithm_global_relation_ids(ctx: TranslationCtx, alg_task: ir.Sequence) -> list[lqp.RelationId]:
177
+ result = []
178
+ updates = collect_by_type(ir.Update, alg_task)
179
+ for update in updates:
180
+ if has_global_annotation(update):
181
+ bindings = _effect_bindings(update)
182
+ projection, _ = _translate_bindings(ctx, bindings)
183
+ rel_id = get_relation_id(ctx, update.relation, projection)
184
+ result.append(rel_id)
185
+ return list(dict.fromkeys(result))
186
+
187
+ def _translate_script(ctx: TranslationCtx, task: ir.Sequence) -> lqp.Script:
188
+ assert is_script(task), "Expected a @script Sequence"
189
+
190
+ constructs: list[lqp.Construct] = []
191
+
192
+ for subtask in task.tasks:
193
+ if is_empty_instruction(subtask):
194
+ constructs.append(_translate_empty_instruction(ctx, subtask))
195
+ elif isinstance(subtask, ir.Logical):
196
+ constructs.extend(_translate_instruction(ctx, subtask))
197
+ elif isinstance(subtask, ir.Break):
198
+ constructs.append(_translate_break_instruction(ctx, subtask))
199
+ elif is_while_loop(subtask):
200
+ constructs.append(_translate_while_loop(ctx, subtask))
201
+ else:
202
+ raise Exception(f"Unsupported script instruction: {subtask}")
203
+
204
+ return lqp.Script(constructs=constructs, meta=None)
205
+
206
+ def _translate_while_loop(ctx: TranslationCtx, task: ir.Loop) -> lqp.Loop:
207
+ assert is_while_loop(task), "Expected a @while Loop"
208
+ assert len(task.iter) == 0, "Temporalized loops not supported"
209
+
210
+ while_script_task = task.body
211
+ assert is_while_script(while_script_task), "The body of a @while Loop must be a @while @script Sequence"
212
+ body_script = _translate_script(ctx, while_script_task)
213
+
214
+ # No init instructions in the translation of PyRel Loops to to LQP loops
215
+ return lqp.Loop(init=[], body=body_script, meta=None)
216
+
217
+ def _translate_break_instruction(ctx: TranslationCtx, task: ir.Break) -> lqp.Construct:
218
+ body = _translate_to_formula(ctx, task.check)
219
+
220
+ ctx.break_rule_counter += 1
221
+
222
+ rel_id = gen_rel_id(ctx, "break_cond_" + str(ctx.break_rule_counter))
223
+ return lqp.Break(
224
+ name = rel_id,
225
+ body = mk_abstraction([], body),
226
+ attrs = [],
227
+ meta = None,
228
+ )
229
+
230
+ def _translate_empty_instruction(ctx: TranslationCtx, rule: ir.Logical) -> lqp.Instruction:
231
+ assert is_empty_instruction(rule)
232
+ updates = collect_by_type(ir.Update, rule)
233
+ assert len(updates) == 1
234
+ update = updates[0]
235
+ bindings = _effect_bindings(update)
236
+
237
+ # We need to make sure that variable names have a leading underscore
238
+ normalized_bindings:Sequence[ir.Var] = []
239
+ for v in bindings:
240
+ assert isinstance(v, ir.Var)
241
+ if not v.name.startswith("_"):
242
+ v = ir.Var(v.type, "_" + v.name)
243
+ normalized_bindings.append(v)
244
+
245
+ projection, eqs = _translate_bindings(ctx, normalized_bindings)
246
+ assert len(eqs) == 0
247
+ rel_id = get_relation_id(ctx, update.relation, projection)
248
+ abstraction = mk_abstraction(projection, mk_or([])) # empty body = false
249
+ return lqp.Assign(name = rel_id, body = abstraction, attrs = [], meta = None)
250
+
251
+ def _translate_instruction(ctx: TranslationCtx, rule: ir.Logical) -> list[lqp.Instruction]:
252
+ effects = collect_by_type((ir.Update, ir.Output), rule)
147
253
 
148
- # TODO: should this ever actually come in as input?
149
254
  if len(effects) == 0:
150
255
  return []
151
256
 
152
- assert len(ranks) == 0 or len(aggregates) == 0, "rules cannot have both aggregates and ranks"
257
+ conjuncts = _translate_to_formula(ctx, rule)
258
+ res = []
259
+ for effect in effects:
260
+ assert isinstance(effect, ir.Update), f"Got an effect of type {type(effect)} in a loop, which is invalid."
153
261
 
154
- conjuncts = []
155
- for task in rule.body:
156
- if isinstance(task, (ir.Output, ir.Update)):
157
- continue
158
- conjuncts.append(_translate_to_formula(ctx, task))
262
+ bindings = _effect_bindings(effect)
263
+ projection, eqs = _translate_bindings(ctx, bindings)
264
+
265
+ eqs.append(conjuncts)
266
+ new_body = mk_and(eqs)
267
+
268
+ rel_id = get_relation_id(ctx, effect.relation, projection)
269
+ abstraction = mk_abstraction(projection, new_body)
270
+
271
+ upsert = get_upsert_annotation(effect)
272
+ monoid = get_monoid_annotation(effect)
273
+ monus = get_monus_annotation(effect)
274
+
275
+ if has_assign_annotation(effect):
276
+ res.append(lqp.Assign(name = rel_id, body = abstraction, attrs = [], meta = None))
277
+ elif upsert is not None:
278
+ res.append(lqp.Upsert(value_arity=get_arity(upsert), name = rel_id, body = abstraction, attrs = [], meta = None))
279
+ elif monoid is not None:
280
+ res.append(lqp.MonoidDef(
281
+ value_arity=get_arity(monoid),
282
+ monoid=construct_monoid(monoid),
283
+ name = rel_id,
284
+ body = abstraction,
285
+ attrs = [],
286
+ meta = None
287
+ ))
288
+ elif monus is not None:
289
+ res.append(lqp.MonusDef(
290
+ value_arity=get_arity(monus),
291
+ monoid=construct_monoid(monus),
292
+ name = rel_id,
293
+ body = abstraction,
294
+ attrs = [],
295
+ meta = None
296
+ ))
297
+
298
+ return res
159
299
 
160
- # Aggregates reduce over the body
161
- if aggregates or ranks:
162
- aggr_body = mk_and(conjuncts)
163
- conjuncts = []
164
- for aggr in aggregates:
165
- conjuncts.append(_translate_aggregate(ctx, aggr, aggr_body))
166
- for rank in ranks:
167
- conjuncts.append(_translate_rank(ctx, rank, aggr_body))
300
+ def _translate_to_standard_decl(ctx: TranslationCtx, rule: ir.Logical) -> list[lqp.Declaration]:
301
+ effects = collect_by_type((ir.Output, ir.Update), rule)
168
302
 
169
- return [_translate_effect(ctx, effect, mk_and(conjuncts)) for effect in effects]
303
+ # TODO: should this ever actually come in as input?
304
+ if len(effects) == 0:
305
+ return []
306
+
307
+ conjuncts = _translate_to_formula(ctx, rule)
308
+ return [_translate_effect(ctx, effect, conjuncts) for effect in effects]
170
309
 
171
310
  def _translate_annotations(annotations: FrozenOrderedSet[ir.Annotation]) -> list[lqp.Attribute]:
172
311
  attributes = []
173
312
  for annotation in annotations:
174
313
 
175
- if annotation.relation.name in lqp_builtins.annotations_to_emit:
314
+ if annotation.relation.name in supported_lqp_annotations:
176
315
  if any(not isinstance(a, ir.Literal) for a in annotation.args):
177
316
  warn("LQP currently ignores annotation parameters with non-literal values")
178
317
  continue
@@ -191,14 +330,11 @@ def _translate_annotations(annotations: FrozenOrderedSet[ir.Annotation]) -> list
191
330
  def _translate_effect(ctx: TranslationCtx, effect: Union[ir.Output, ir.Update], body: lqp.Formula) -> lqp.Declaration:
192
331
  bindings = _effect_bindings(effect)
193
332
 
194
- def _is_export(e):
195
- return isinstance(e, ir.Output) and builtins.export_annotation in e.annotations
196
-
197
333
  if isinstance(effect, ir.Output):
198
334
  projection, eqs, suffix = _translate_output_bindings(ctx, bindings)
199
335
  meta_id = effect.id
200
336
 
201
- if _is_export(effect):
337
+ if helpers.is_export(effect):
202
338
  def_name = "export_relation" + suffix
203
339
  else:
204
340
  def_name = "output" + suffix
@@ -214,14 +350,23 @@ def _translate_effect(ctx: TranslationCtx, effect: Union[ir.Output, ir.Update],
214
350
  new_body = mk_and(eqs)
215
351
 
216
352
  # Context bookkeeping for exports and outputs
217
- if _is_export(effect):
353
+ if helpers.is_export(effect):
218
354
  # The row id is the first n-1 elements, and the actual data is the last element. Its
219
355
  # type is stored in the first element of the tuple.
220
356
  col_type = projection[-1][1]
221
357
  _col_num_match = re.search(r"export_relation_col([0-9]+)", def_name)
222
358
  assert _col_num_match, f"Could not find column number in suffix: {def_name}"
223
359
  col_num = int(_col_num_match.group(1))
224
- ctx.export_ids.append((rel_id, col_num, col_type))
360
+ col_name = f"col{col_num}"
361
+ if isinstance(effect, ir.Output) and len(effect.aliases) > 0:
362
+ aliases_list = list(effect.aliases)
363
+ col_name = aliases_list[-1][0]
364
+ ctx.export_descriptors.append(ExportDescriptor(
365
+ relation_id=rel_id,
366
+ column_name=col_name,
367
+ column_number=col_num,
368
+ column_type=col_type,
369
+ ))
225
370
  elif isinstance(effect, ir.Output):
226
371
  ctx.output_ids.append((rel_id, def_name))
227
372
 
@@ -544,7 +689,36 @@ def _translate_aggregate(ctx: TranslationCtx, aggr: ir.Aggregate, body: lqp.Form
544
689
 
545
690
  def _translate_to_formula(ctx: TranslationCtx, task: ir.Task) -> lqp.Formula:
546
691
  if isinstance(task, ir.Logical):
547
- conjuncts = [_translate_to_formula(ctx, child) for child in task.body]
692
+ # For aggregates and ranks, the expected format is:
693
+ #
694
+ # Logical
695
+ # body_task1
696
+ # body_task2
697
+ # ...
698
+ # aggregate/rank task
699
+ #
700
+ # If we see that the Logical is in this format, it should be translated as an
701
+ # aggregate/rank node.
702
+ groups = group_tasks(task.body, {
703
+ "aggregates": ir.Aggregate,
704
+ "ranks": ir.Rank,
705
+ })
706
+
707
+ aggregates = groups.get("aggregates", OrderedSet[ir.Task]())
708
+ ranks = groups.get("ranks", OrderedSet[ir.Task]())
709
+
710
+ if aggregates or ranks:
711
+ conjuncts = []
712
+ body = mk_and([_translate_to_formula(ctx, t) for t in task.body])
713
+ for aggr in aggregates:
714
+ assert(isinstance(aggr, ir.Aggregate))
715
+ conjuncts.append(_translate_aggregate(ctx, aggr, body))
716
+ for rank in ranks:
717
+ assert(isinstance(rank, ir.Rank))
718
+ conjuncts.append(_translate_rank(ctx, rank, body))
719
+ else:
720
+ # If there are no aggregates or ranks, translate as a normal conjunction
721
+ conjuncts = [_translate_to_formula(ctx, child) for child in task.body]
548
722
  return mk_and(conjuncts)
549
723
  elif isinstance(task, ir.Lookup):
550
724
  return _translate_to_atom(ctx, task)
@@ -564,14 +738,13 @@ def _translate_to_formula(ctx: TranslationCtx, task: ir.Task) -> lqp.Formula:
564
738
 
565
739
  return mk_primitive("rel_primitive_hash_tuple_uint128", [v for v, _ in terms])
566
740
  elif isinstance(task, ir.Union):
567
- # TODO: handle hoisted vars if needed
568
741
  disjs = [_translate_to_formula(ctx, child) for child in task.tasks]
569
742
  return mk_or(disjs)
570
- elif isinstance(task, (ir.Aggregate, ir.Output, ir.Update)):
743
+ elif isinstance(task, (ir.Output, ir.Update)):
571
744
  # Nothing to do here, handled in _translate_to_decls
572
745
  return mk_and([])
573
- elif isinstance(task, ir.Rank):
574
- # Nothing to do here, handled in _translate_to_decls
746
+ elif isinstance(task, (ir.Aggregate, ir.Rank)):
747
+ # Nothing to do here, handled at the Logical level
575
748
  return mk_and([])
576
749
  else:
577
750
  raise NotImplementedError(f"Unknown task type (formula): {type(task)}")
@@ -666,7 +839,7 @@ def get_output_id(ctx: TranslationCtx, orig_name: str, metamodel_id: int) -> lqp
666
839
  unique_name = ctx.output_names.get_name_by_id(metamodel_id, orig_name)
667
840
  return utils.gen_rel_id(ctx, unique_name)
668
841
 
669
- def _translate_bindings(ctx: TranslationCtx, bindings: list[ir.Value]) -> Tuple[list[Tuple[lqp.Var, lqp.Type]], list[lqp.Formula]]:
842
+ def _translate_bindings(ctx: TranslationCtx, bindings: Sequence[ir.Value]) -> Tuple[list[Tuple[lqp.Var, lqp.Type]], list[lqp.Formula]]:
670
843
  lqp_vars = []
671
844
  conjuncts = []
672
845
  for binding in bindings: