classiq 0.58.0__py3-none-any.whl → 0.59.0__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 (40) hide show
  1. classiq/_internals/api_wrapper.py +8 -3
  2. classiq/_internals/jobs.py +3 -5
  3. classiq/execution/execution_session.py +36 -20
  4. classiq/executor.py +2 -1
  5. classiq/interface/_version.py +1 -1
  6. classiq/interface/generator/arith/arithmetic_operations.py +1 -0
  7. classiq/interface/generator/register_role.py +8 -0
  8. classiq/interface/model/handle_binding.py +22 -3
  9. classiq/model_expansions/capturing/captured_vars.py +316 -0
  10. classiq/model_expansions/capturing/mangling_utils.py +18 -9
  11. classiq/model_expansions/closure.py +29 -74
  12. classiq/model_expansions/function_builder.py +51 -66
  13. classiq/model_expansions/interpreter.py +4 -7
  14. classiq/model_expansions/quantum_operations/bind.py +1 -3
  15. classiq/model_expansions/quantum_operations/call_emitter.py +46 -11
  16. classiq/model_expansions/quantum_operations/classicalif.py +2 -5
  17. classiq/model_expansions/quantum_operations/control.py +13 -16
  18. classiq/model_expansions/quantum_operations/emitter.py +36 -8
  19. classiq/model_expansions/quantum_operations/expression_operation.py +9 -19
  20. classiq/model_expansions/quantum_operations/inplace_binary_operation.py +4 -6
  21. classiq/model_expansions/quantum_operations/invert.py +5 -8
  22. classiq/model_expansions/quantum_operations/power.py +5 -10
  23. classiq/model_expansions/quantum_operations/quantum_assignment_operation.py +1 -3
  24. classiq/model_expansions/quantum_operations/quantum_function_call.py +1 -3
  25. classiq/model_expansions/quantum_operations/repeat.py +3 -3
  26. classiq/model_expansions/quantum_operations/variable_decleration.py +1 -1
  27. classiq/model_expansions/quantum_operations/within_apply.py +1 -5
  28. classiq/model_expansions/scope.py +2 -2
  29. classiq/model_expansions/transformers/var_splitter.py +32 -19
  30. classiq/model_expansions/utils/handles_collector.py +33 -0
  31. classiq/model_expansions/visitors/variable_references.py +18 -2
  32. classiq/qmod/qfunc.py +9 -13
  33. classiq/qmod/quantum_expandable.py +1 -21
  34. classiq/qmod/quantum_function.py +16 -0
  35. {classiq-0.58.0.dist-info → classiq-0.59.0.dist-info}/METADATA +2 -2
  36. {classiq-0.58.0.dist-info → classiq-0.59.0.dist-info}/RECORD +37 -38
  37. classiq/interface/executor/aws_execution_cost.py +0 -90
  38. classiq/model_expansions/capturing/captured_var_manager.py +0 -48
  39. classiq/model_expansions/capturing/propagated_var_stack.py +0 -194
  40. {classiq-0.58.0.dist-info → classiq-0.59.0.dist-info}/WHEEL +0 -0
@@ -113,14 +113,19 @@ class ApiWrapper:
113
113
  return _parse_job_response(result, generator_result.QuantumProgram)
114
114
 
115
115
  @classmethod
116
- async def call_execute_generated_circuit(
116
+ async def call_convert_quantum_program(
117
117
  cls, circuit: generator_result.QuantumProgram
118
- ) -> execution_request.ExecutionJobDetails:
119
- execution_input = await cls._call_task_pydantic(
118
+ ) -> dict:
119
+ return await cls._call_task_pydantic(
120
120
  http_method=HTTPMethod.POST,
121
121
  url=routes.CONVERSION_GENERATED_CIRCUIT_TO_EXECUTION_INPUT_FULL,
122
122
  model=circuit,
123
123
  )
124
+
125
+ @classmethod
126
+ async def call_execute_execution_input(
127
+ cls, execution_input: dict
128
+ ) -> execution_request.ExecutionJobDetails:
124
129
  headers = {
125
130
  _ACCEPT_HEADER: "v1",
126
131
  _CONTENT_TYPE_HEADER: execution_input["version"],
@@ -45,9 +45,9 @@ def _general_job_description_parser(
45
45
 
46
46
 
47
47
  class JobPoller:
48
- INITIAL_INTERVAL_SEC = 1
49
- INTERVAL_FACTOR = 2
50
- FINAL_INTERVAL_SEC = INITIAL_INTERVAL_SEC * INTERVAL_FACTOR**5 # 32 secs
48
+ INITIAL_INTERVAL_SEC = 0.1
49
+ INTERVAL_FACTOR = 1.5
50
+ FINAL_INTERVAL_SEC = 25
51
51
  DEV_INTERVAL = 0.05
52
52
 
53
53
  def __init__(
@@ -107,8 +107,6 @@ class JobPoller:
107
107
  while True:
108
108
  yield self.DEV_INTERVAL
109
109
  else:
110
- for _ in range(10):
111
- yield self.INITIAL_INTERVAL_SEC
112
110
  interval = self.INITIAL_INTERVAL_SEC
113
111
  while True:
114
112
  yield interval
@@ -1,4 +1,5 @@
1
1
  import json
2
+ from functools import cached_property
2
3
  from typing import Any, Callable, Optional, Union, cast
3
4
 
4
5
  import numpy as np
@@ -15,12 +16,13 @@ from classiq.interface.executor.result import (
15
16
  from classiq.interface.generator.functions.qmod_python_interface import QmodPyStruct
16
17
  from classiq.interface.generator.quantum_program import QuantumProgram
17
18
 
19
+ from classiq._internals import async_utils
20
+ from classiq._internals.api_wrapper import ApiWrapper
18
21
  from classiq.applications.combinatorial_helpers.pauli_helpers.pauli_utils import (
19
22
  _pauli_dict_to_pauli_terms,
20
23
  _pauli_terms_to_qmod,
21
24
  )
22
25
  from classiq.execution.jobs import ExecutionJob
23
- from classiq.executor import execute
24
26
  from classiq.qmod.builtins import PauliTerm
25
27
  from classiq.qmod.builtins.classical_execution_primitives import (
26
28
  CARRAY_SEPARATOR,
@@ -158,14 +160,28 @@ class ExecutionSession:
158
160
  ):
159
161
  self.program: QuantumProgram = _deserialize_program(quantum_program)
160
162
  self.update_execution_preferences(execution_preferences)
163
+ # When the primitives are called, we always override the
164
+ # classical_execution_code, and we don't want the conversion route to fail
165
+ # because cmain is expected in some cases
166
+ self.program.model.classical_execution_code = "dummy"
161
167
 
162
- @property
163
- def qprog(self) -> SerializedQuantumProgram:
164
- """
165
- Returns:
166
- SerializedQuantumProgram: The serialized quantum program (str). See `QuantumProgram`.
167
- """
168
- return SerializedQuantumProgram(self.program.model_dump_json(indent=2))
168
+ @cached_property
169
+ def _execution_input(self) -> dict:
170
+ return async_utils.run(ApiWrapper.call_convert_quantum_program(self.program))
171
+
172
+ def _execute(
173
+ self, classical_execution_code: str, primitives_input: PrimitivesInput
174
+ ) -> ExecutionJob:
175
+ execution_input = self._execution_input.copy()
176
+ execution_input["classical_execution_code"] = classical_execution_code
177
+ # The use of `model_dump_json` is necessary for complex numbers serialization
178
+ execution_input["primitives_input"] = json.loads(
179
+ primitives_input.model_dump_json()
180
+ )
181
+ result = async_utils.run(
182
+ ApiWrapper.call_execute_execution_input(execution_input)
183
+ )
184
+ return ExecutionJob(details=result)
169
185
 
170
186
  def update_execution_preferences(
171
187
  self, execution_preferences: Optional[ExecutionPreferences]
@@ -210,13 +226,13 @@ class ExecutionSession:
210
226
  Returns:
211
227
  The execution job.
212
228
  """
213
- self.program.model.classical_execution_code = generate_code_snippet(
229
+ classical_execution_code = generate_code_snippet(
214
230
  SupportedPrimitives.SAMPLE, parameters=format_parameters(parameters)
215
231
  )
216
- self.program.execution_primitives_input = PrimitivesInput(
232
+ execution_primitives_input = PrimitivesInput(
217
233
  sample=[parse_params(parameters)] if parameters is not None else [{}]
218
234
  )
219
- return execute(SerializedQuantumProgram(self.qprog))
235
+ return self._execute(classical_execution_code, execution_primitives_input)
220
236
 
221
237
  def batch_sample(self, parameters: list[ExecutionParams]) -> list[ExecutionDetails]:
222
238
  """
@@ -244,13 +260,13 @@ class ExecutionSession:
244
260
  Returns:
245
261
  The execution job.
246
262
  """
247
- self.program.model.classical_execution_code = generate_code_snippet(
263
+ classical_execution_code = generate_code_snippet(
248
264
  SupportedPrimitives.BATCH_SAMPLE, parameters=format_parameters(parameters)
249
265
  )
250
- self.program.execution_primitives_input = PrimitivesInput(
266
+ execution_primitives_input = PrimitivesInput(
251
267
  sample=[parse_params(params) for params in parameters]
252
268
  )
253
- return execute(SerializedQuantumProgram(self.qprog))
269
+ return self._execute(classical_execution_code, execution_primitives_input)
254
270
 
255
271
  def estimate(
256
272
  self, hamiltonian: Hamiltonian, parameters: Optional[ExecutionParams] = None
@@ -284,12 +300,12 @@ class ExecutionSession:
284
300
  Returns:
285
301
  The execution job.
286
302
  """
287
- self.program.model.classical_execution_code = generate_code_snippet(
303
+ classical_execution_code = generate_code_snippet(
288
304
  SupportedPrimitives.ESTIMATE,
289
305
  parameters=format_parameters(parameters),
290
306
  hamiltonian=to_hamiltonian_str(hamiltonian),
291
307
  )
292
- self.program.execution_primitives_input = PrimitivesInput(
308
+ execution_primitives_input = PrimitivesInput(
293
309
  estimate=EstimateInput(
294
310
  hamiltonian=_hamiltonian_to_pauli_operator(hamiltonian),
295
311
  parameters=(
@@ -297,7 +313,7 @@ class ExecutionSession:
297
313
  ),
298
314
  )
299
315
  )
300
- return execute(SerializedQuantumProgram(self.qprog))
316
+ return self._execute(classical_execution_code, execution_primitives_input)
301
317
 
302
318
  def batch_estimate(
303
319
  self, hamiltonian: Hamiltonian, parameters: list[ExecutionParams]
@@ -331,18 +347,18 @@ class ExecutionSession:
331
347
  Returns:
332
348
  The execution job.
333
349
  """
334
- self.program.model.classical_execution_code = generate_code_snippet(
350
+ classical_execution_code = generate_code_snippet(
335
351
  SupportedPrimitives.BATCH_ESTIMATE,
336
352
  parameters=format_parameters(parameters),
337
353
  hamiltonian=to_hamiltonian_str(hamiltonian),
338
354
  )
339
- self.program.execution_primitives_input = PrimitivesInput(
355
+ execution_primitives_input = PrimitivesInput(
340
356
  estimate=EstimateInput(
341
357
  hamiltonian=_hamiltonian_to_pauli_operator(hamiltonian),
342
358
  parameters=[parse_params(params) for params in parameters],
343
359
  )
344
360
  )
345
- return execute(SerializedQuantumProgram(self.qprog))
361
+ return self._execute(classical_execution_code, execution_primitives_input)
346
362
 
347
363
  def estimate_cost(
348
364
  self,
classiq/executor.py CHANGED
@@ -32,7 +32,8 @@ def _parse_serialized_qprog(
32
32
 
33
33
  async def execute_async(quantum_program: SerializedQuantumProgram) -> ExecutionJob:
34
34
  circuit = _parse_serialized_qprog(quantum_program)
35
- result = await ApiWrapper.call_execute_generated_circuit(circuit)
35
+ execution_input = await ApiWrapper.call_convert_quantum_program(circuit)
36
+ result = await ApiWrapper.call_execute_execution_input(execution_input)
36
37
  return ExecutionJob(details=result)
37
38
 
38
39
 
@@ -3,5 +3,5 @@ from packaging.version import Version
3
3
  # This file was generated automatically
4
4
  # Please don't track in version control (DONTTRACK)
5
5
 
6
- SEMVER_VERSION = '0.58.0'
6
+ SEMVER_VERSION = '0.59.0'
7
7
  VERSION = str(Version(SEMVER_VERSION))
@@ -10,6 +10,7 @@ from classiq.interface.generator.arith.machine_precision import (
10
10
  from classiq.interface.generator.arith.register_user_input import RegisterArithmeticInfo
11
11
  from classiq.interface.generator.function_params import FunctionParams
12
12
 
13
+ IMPLICIT_OUTPUTS: Final[str] = "implicit_outputs"
13
14
  DEFAULT_GARBAGE_OUT_NAME: Final[str] = "extra_qubits"
14
15
  MODULO_WITH_FRACTION_PLACES_ERROR_MSG: Final[str] = (
15
16
  "Modulo with fraction places not supported"
@@ -29,3 +29,11 @@ class RegisterRole(StrEnum):
29
29
  RegisterRole.AUXILIARY,
30
30
  RegisterRole.EXPLICIT_ZERO_INPUT,
31
31
  }
32
+
33
+ @staticmethod
34
+ def clean_output_roles() -> set["RegisterRole"]:
35
+ return {RegisterRole.ZERO_OUTPUT, RegisterRole.AUXILIARY}
36
+
37
+ @staticmethod
38
+ def dirty_output_roles() -> set["RegisterRole"]:
39
+ return {RegisterRole.OUTPUT, RegisterRole.GARBAGE_OUTPUT}
@@ -19,6 +19,10 @@ class HandleBinding(ASTNode):
19
19
  def __str__(self) -> str:
20
20
  return self.name
21
21
 
22
+ @property
23
+ def qmod_expr(self) -> str:
24
+ return self.name
25
+
22
26
  def is_bindable(self) -> bool:
23
27
  return True
24
28
 
@@ -57,6 +61,9 @@ class HandleBinding(ASTNode):
57
61
  return replacement
58
62
  return self
59
63
 
64
+ def __contains__(self, other_handle: "HandleBinding") -> bool:
65
+ return self.collapse() in other_handle.collapse().prefixes()
66
+
60
67
 
61
68
  class NestedHandleBinding(HandleBinding):
62
69
  base_handle: "ConcreteHandleBinding"
@@ -66,8 +73,8 @@ class NestedHandleBinding(HandleBinding):
66
73
  def _set_name(cls, values: Any) -> dict[str, Any]:
67
74
  if isinstance(values, dict):
68
75
  orig = values
69
- while "base_handle" in values:
70
- values = values["base_handle"]
76
+ while "base_handle" in dict(values):
77
+ values = dict(values)["base_handle"]
71
78
  orig["name"] = dict(values).get("name")
72
79
  return orig
73
80
  if isinstance(values, NestedHandleBinding):
@@ -106,6 +113,10 @@ class SubscriptHandleBinding(NestedHandleBinding):
106
113
  def __str__(self) -> str:
107
114
  return f"{self.base_handle}[{self.index}]"
108
115
 
116
+ @property
117
+ def qmod_expr(self) -> str:
118
+ return f"{self.base_handle.qmod_expr}[{self.index}]"
119
+
109
120
  @property
110
121
  def identifier(self) -> str:
111
122
  return f"{self.base_handle.identifier}{HANDLE_ID_SEPARATOR}{self.index}"
@@ -157,6 +168,10 @@ class SlicedHandleBinding(NestedHandleBinding):
157
168
  def __str__(self) -> str:
158
169
  return f"{self.base_handle}[{self.start}:{self.end}]"
159
170
 
171
+ @property
172
+ def qmod_expr(self) -> str:
173
+ return f"{self.base_handle.qmod_expr}[{self.start}:{self.end}]"
174
+
160
175
  @property
161
176
  def identifier(self) -> str:
162
177
  return (
@@ -165,7 +180,7 @@ class SlicedHandleBinding(NestedHandleBinding):
165
180
 
166
181
  def collapse(self) -> HandleBinding:
167
182
  if isinstance(self.base_handle, SlicedHandleBinding):
168
- return SubscriptHandleBinding(
183
+ return SlicedHandleBinding(
169
184
  base_handle=self.base_handle.base_handle,
170
185
  start=self._get_collapsed_start(),
171
186
  end=self._get_collapsed_stop(),
@@ -224,6 +239,10 @@ class FieldHandleBinding(NestedHandleBinding):
224
239
  def __str__(self) -> str:
225
240
  return f"{self.base_handle}.{self.field}"
226
241
 
242
+ @property
243
+ def qmod_expr(self) -> str:
244
+ return f"get_field({self.base_handle.qmod_expr}, '{self.field}')"
245
+
227
246
  @property
228
247
  def identifier(self) -> str:
229
248
  return f"{self.base_handle.identifier}{HANDLE_ID_SEPARATOR}{self.field}"
@@ -0,0 +1,316 @@
1
+ import dataclasses
2
+ from collections.abc import Iterator, Sequence
3
+ from contextlib import contextmanager
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING
6
+
7
+ from classiq.interface.enum_utils import StrEnum
8
+ from classiq.interface.exceptions import (
9
+ ClassiqExpansionError,
10
+ ClassiqInternalExpansionError,
11
+ )
12
+ from classiq.interface.generator.functions.port_declaration import (
13
+ PortDeclarationDirection,
14
+ )
15
+ from classiq.interface.model.handle_binding import HandleBinding, NestedHandleBinding
16
+ from classiq.interface.model.port_declaration import PortDeclaration
17
+ from classiq.interface.model.quantum_function_call import ArgValue
18
+ from classiq.interface.model.quantum_type import QuantumType
19
+
20
+ from classiq.model_expansions.capturing.mangling_utils import (
21
+ demangle_handle,
22
+ mangle_captured_var_name,
23
+ )
24
+ from classiq.model_expansions.scope import QuantumSymbol
25
+ from classiq.model_expansions.transformers.var_splitter import SymbolPart, SymbolParts
26
+
27
+ if TYPE_CHECKING:
28
+ from classiq.model_expansions.closure import FunctionClosure
29
+
30
+
31
+ class PortDirection(StrEnum):
32
+ Input = "input"
33
+ Inout = "inout"
34
+ Output = "output"
35
+ Outin = "outin"
36
+
37
+ def negate(self) -> "PortDirection":
38
+ if self == PortDirection.Input:
39
+ return PortDirection.Output
40
+ if self == PortDirection.Output:
41
+ return PortDirection.Input
42
+ return self
43
+
44
+ @staticmethod
45
+ def load(direction: PortDeclarationDirection) -> "PortDirection":
46
+ if direction == PortDeclarationDirection.Input:
47
+ return PortDirection.Input
48
+ if direction == PortDeclarationDirection.Output:
49
+ return PortDirection.Output
50
+ if direction == PortDeclarationDirection.Inout:
51
+ return PortDirection.Inout
52
+ raise ClassiqInternalExpansionError
53
+
54
+ def dump(self) -> PortDeclarationDirection:
55
+ if self == PortDirection.Input:
56
+ return PortDeclarationDirection.Input
57
+ if self == PortDirection.Output:
58
+ return PortDeclarationDirection.Output
59
+ if self == PortDirection.Inout:
60
+ return PortDeclarationDirection.Inout
61
+ raise ClassiqInternalExpansionError
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class _CapturedHandle:
66
+ handle: HandleBinding
67
+ quantum_type: QuantumType
68
+ defining_function: "FunctionClosure"
69
+ direction: PortDirection
70
+ is_propagated: bool
71
+
72
+ @property
73
+ def mangled_name(self) -> str:
74
+ return mangle_captured_var_name(self.handle.identifier, self.defining_function)
75
+
76
+ @property
77
+ def port(self) -> PortDeclaration:
78
+ return PortDeclaration(
79
+ name=self.mangled_name,
80
+ quantum_type=self.quantum_type,
81
+ direction=self.direction.dump(),
82
+ )
83
+
84
+ def is_same_var(self, other: "_CapturedHandle") -> bool:
85
+ return self.handle.name == other.handle.name and _same_closure(
86
+ self.defining_function, other.defining_function
87
+ )
88
+
89
+ def change_direction(self, new_direction: PortDirection) -> "_CapturedHandle":
90
+ return dataclasses.replace(self, direction=new_direction)
91
+
92
+ def set_propagated(self) -> "_CapturedHandle":
93
+ return dataclasses.replace(self, is_propagated=True)
94
+
95
+ def update_propagation(
96
+ self, other_captured_handle: "_CapturedHandle"
97
+ ) -> "_CapturedHandle":
98
+ if self.is_propagated and not other_captured_handle.is_propagated:
99
+ return dataclasses.replace(self, is_propagated=False)
100
+ return self
101
+
102
+
103
+ @dataclass
104
+ class CapturedVars:
105
+ _captured_handles: list[_CapturedHandle] = field(default_factory=list)
106
+
107
+ def capture_handle(
108
+ self,
109
+ handle: HandleBinding,
110
+ quantum_type: QuantumType,
111
+ defining_function: "FunctionClosure",
112
+ direction: PortDeclarationDirection,
113
+ ) -> None:
114
+ self._capture_handle(
115
+ _CapturedHandle(
116
+ handle=handle,
117
+ quantum_type=quantum_type,
118
+ defining_function=defining_function,
119
+ direction=PortDirection.load(direction),
120
+ is_propagated=False,
121
+ )
122
+ )
123
+
124
+ def _capture_handle(self, captured_handle: _CapturedHandle) -> None:
125
+ if (
126
+ isinstance(captured_handle.handle, NestedHandleBinding)
127
+ and captured_handle.direction != PortDirection.Inout
128
+ ):
129
+ raise ClassiqInternalExpansionError("Captured nested handles must be inout")
130
+
131
+ new_captured_handles = []
132
+ for existing_captured_handle in self._captured_handles:
133
+ if not existing_captured_handle.is_same_var(captured_handle):
134
+ new_captured_handles.append(existing_captured_handle)
135
+ continue
136
+ captured_handle = captured_handle.update_propagation(
137
+ existing_captured_handle
138
+ )
139
+ if existing_captured_handle.handle == captured_handle.handle:
140
+ captured_handle = self._conjugate_direction(
141
+ existing_captured_handle, captured_handle
142
+ )
143
+ elif captured_handle.handle in existing_captured_handle.handle:
144
+ if existing_captured_handle.direction in (
145
+ PortDirection.Input,
146
+ PortDirection.Outin,
147
+ ):
148
+ raise ClassiqInternalExpansionError(
149
+ "Captured handle is already freed"
150
+ )
151
+ captured_handle = existing_captured_handle
152
+ elif existing_captured_handle.handle in captured_handle.handle:
153
+ if captured_handle.direction in (
154
+ PortDirection.Output,
155
+ PortDirection.Outin,
156
+ ):
157
+ raise ClassiqInternalExpansionError(
158
+ "Captured handle is already allocated"
159
+ )
160
+ else:
161
+ new_captured_handles.append(existing_captured_handle)
162
+ new_captured_handles.append(captured_handle)
163
+ self._captured_handles = new_captured_handles
164
+
165
+ def _conjugate_direction(
166
+ self,
167
+ existing_captured_handle: _CapturedHandle,
168
+ captured_handle: _CapturedHandle,
169
+ ) -> _CapturedHandle:
170
+ if existing_captured_handle.direction == PortDirection.Input:
171
+ if captured_handle.direction == PortDirection.Output:
172
+ return captured_handle.change_direction(PortDirection.Inout)
173
+ if captured_handle.direction == PortDirection.Outin:
174
+ return captured_handle.change_direction(PortDirection.Input)
175
+ raise ClassiqInternalExpansionError("Captured handle is already freed")
176
+ if existing_captured_handle.direction == PortDirection.Output:
177
+ if captured_handle.direction == PortDirection.Input:
178
+ return captured_handle.change_direction(PortDirection.Outin)
179
+ if captured_handle.direction in (
180
+ PortDirection.Output,
181
+ PortDirection.Outin,
182
+ ):
183
+ raise ClassiqInternalExpansionError(
184
+ "Captured handle is already allocated"
185
+ )
186
+ return captured_handle.change_direction(PortDirection.Output)
187
+ if existing_captured_handle.direction == PortDirection.Inout:
188
+ if captured_handle.direction in (
189
+ PortDirection.Output,
190
+ PortDirection.Outin,
191
+ ):
192
+ raise ClassiqInternalExpansionError(
193
+ "Captured handle is already allocated"
194
+ )
195
+ elif captured_handle.direction in (
196
+ PortDirection.Input,
197
+ PortDirection.Inout,
198
+ ):
199
+ raise ClassiqInternalExpansionError("Captured handle is already freed")
200
+ return captured_handle
201
+
202
+ def update(self, other_captured_vars: "CapturedVars") -> None:
203
+ for captured_handle in other_captured_vars._captured_handles:
204
+ self._capture_handle(captured_handle)
205
+
206
+ def negate(self) -> "CapturedVars":
207
+ return CapturedVars(
208
+ _captured_handles=[
209
+ captured_handle.change_direction(captured_handle.direction.negate())
210
+ for captured_handle in self._captured_handles
211
+ ]
212
+ )
213
+
214
+ def filter(self, current_function: "FunctionClosure") -> "CapturedVars":
215
+ return CapturedVars(
216
+ _captured_handles=[
217
+ captured_handle
218
+ for captured_handle in self._captured_handles
219
+ if not _same_closure(
220
+ captured_handle.defining_function, current_function
221
+ )
222
+ ]
223
+ )
224
+
225
+ def set_propagated(self) -> "CapturedVars":
226
+ return CapturedVars(
227
+ _captured_handles=[
228
+ captured_handle.set_propagated()
229
+ for captured_handle in self._captured_handles
230
+ ]
231
+ )
232
+
233
+ def get_captured_ports(self) -> list[PortDeclaration]:
234
+ return [captured_handle.port for captured_handle in self._captured_handles]
235
+
236
+ def get_captured_args(
237
+ self, current_function: "FunctionClosure"
238
+ ) -> list[HandleBinding]:
239
+ return [
240
+ (
241
+ captured_handle.handle
242
+ if _same_closure(current_function, captured_handle.defining_function)
243
+ else HandleBinding(name=captured_handle.mangled_name)
244
+ )
245
+ for captured_handle in self._captured_handles
246
+ ]
247
+
248
+ def get_captured_mapping(self) -> SymbolParts:
249
+ return {
250
+ QuantumSymbol(
251
+ handle=captured_handle.handle,
252
+ quantum_type=captured_handle.quantum_type,
253
+ ): [
254
+ SymbolPart(
255
+ source_handle=captured_handle.handle,
256
+ target_var_name=captured_handle.mangled_name,
257
+ target_var_type=captured_handle.quantum_type,
258
+ )
259
+ ]
260
+ for captured_handle in self._captured_handles
261
+ if not captured_handle.is_propagated
262
+ }
263
+
264
+ @contextmanager
265
+ def freeze(self) -> Iterator[None]:
266
+ previous = self._captured_handles
267
+ yield
268
+ self._captured_handles = previous
269
+
270
+
271
+ def _same_closure(closure_1: "FunctionClosure", closure_2: "FunctionClosure") -> bool:
272
+ return closure_1.depth == closure_2.depth
273
+
274
+
275
+ def validate_args_are_not_propagated(
276
+ args: Sequence[ArgValue], captured_vars: Sequence[HandleBinding]
277
+ ) -> None:
278
+ if not captured_vars:
279
+ return
280
+ captured_handles = {demangle_handle(handle) for handle in captured_vars}
281
+ arg_handles = {
282
+ demangle_handle(arg) for arg in args if isinstance(arg, HandleBinding)
283
+ }
284
+ if any(
285
+ arg_handle.overlaps(captured_handle)
286
+ for arg_handle in arg_handles
287
+ for captured_handle in captured_handles
288
+ ):
289
+ captured_handles_str = {str(handle) for handle in captured_handles}
290
+ arg_handles_str = {str(handle) for handle in arg_handles}
291
+ vars_msg = f"Explicitly passed variables: {arg_handles_str}, captured variables: {captured_handles_str}"
292
+ raise ClassiqExpansionError(
293
+ f"Cannot capture variables that are explicitly passed as arguments. "
294
+ f"{vars_msg}"
295
+ )
296
+
297
+
298
+ def validate_captured_directions(captured_vars: CapturedVars) -> None:
299
+ captured_inputs = [
300
+ captured_handle.handle.name
301
+ for captured_handle in captured_vars._captured_handles
302
+ if captured_handle.direction == PortDirection.Input
303
+ ]
304
+ captured_outputs = [
305
+ captured_handle.handle.name
306
+ for captured_handle in captured_vars._captured_handles
307
+ if captured_handle.direction == PortDirection.Output
308
+ ]
309
+ if len(captured_inputs) > 0:
310
+ raise ClassiqExpansionError(
311
+ f"Captured quantum variables {captured_inputs!r} cannot be used as inputs"
312
+ )
313
+ if len(captured_outputs) > 0:
314
+ raise ClassiqExpansionError(
315
+ f"Captured quantum variables {captured_outputs!r} cannot be used as outputs"
316
+ )
@@ -1,17 +1,25 @@
1
1
  import re
2
+ from typing import TYPE_CHECKING
2
3
 
3
4
  from classiq.interface.generator.compiler_keywords import CAPTURE_SUFFIX
4
5
  from classiq.interface.model.handle_binding import HANDLE_ID_SEPARATOR, HandleBinding
5
6
 
7
+ if TYPE_CHECKING:
8
+ from classiq.model_expansions.closure import FunctionClosure
9
+
6
10
  IDENTIFIER_PATTERN = r"[a-zA-Z_][a-zA-Z0-9_]*"
7
11
  CAPTURE_PATTERN = re.compile(
8
- rf"({IDENTIFIER_PATTERN}){CAPTURE_SUFFIX}{IDENTIFIER_PATTERN}__"
12
+ rf"({IDENTIFIER_PATTERN}){CAPTURE_SUFFIX}{IDENTIFIER_PATTERN}__\d*"
9
13
  )
10
14
  ARRAY_CAST_SUFFIX = HANDLE_ID_SEPARATOR + "array_cast"
11
15
 
12
16
 
13
- def mangle_captured_var_name(var_name: str, defining_function: str) -> str:
14
- return f"{var_name}{CAPTURE_SUFFIX}{defining_function}__"
17
+ def mangle_captured_var_name(
18
+ var_name: str, defining_function: "FunctionClosure"
19
+ ) -> str:
20
+ return (
21
+ f"{var_name}{CAPTURE_SUFFIX}{defining_function.name}__{defining_function.depth}"
22
+ )
15
23
 
16
24
 
17
25
  def demangle_name(name: str) -> str:
@@ -25,15 +33,16 @@ def demangle_handle(handle: HandleBinding) -> HandleBinding:
25
33
  return handle
26
34
  if ARRAY_CAST_SUFFIX in name:
27
35
  return HandleBinding(name=name.split(ARRAY_CAST_SUFFIX)[0])
28
- name = re.sub(r"_\d+$", "", name)
36
+ name = re.sub(r"([^_])_\d+$", r"\1", name)
29
37
  name_parts = name.split(HANDLE_ID_SEPARATOR)
30
- new_name = name_parts[0]
38
+ new_name_parts = [name_parts[0]]
31
39
  for part in name_parts[1:]:
32
40
  if re.fullmatch(r"\d+", part):
33
- new_name += f"[{part}]"
41
+ new_name_parts.append(f"[{part}]")
34
42
  elif re.fullmatch(r"\d+_\d+", part):
35
43
  part_left, part_right = part.split("_")
36
- new_name += f"[{part_left}:{part_right}]"
44
+ new_name_parts.append(f"[{part_left}:{part_right}]")
37
45
  else:
38
- new_name += f".{part}"
39
- return handle.rename(new_name)
46
+ new_name_parts.append(f".{part}")
47
+ new_name_parts = list(map(demangle_name, new_name_parts))
48
+ return handle.rename("".join(new_name_parts))