guppylang-internals 0.21.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 (98) hide show
  1. guppylang_internals/__init__.py +3 -0
  2. guppylang_internals/ast_util.py +350 -0
  3. guppylang_internals/cfg/__init__.py +0 -0
  4. guppylang_internals/cfg/analysis.py +230 -0
  5. guppylang_internals/cfg/bb.py +221 -0
  6. guppylang_internals/cfg/builder.py +606 -0
  7. guppylang_internals/cfg/cfg.py +117 -0
  8. guppylang_internals/checker/__init__.py +0 -0
  9. guppylang_internals/checker/cfg_checker.py +388 -0
  10. guppylang_internals/checker/core.py +550 -0
  11. guppylang_internals/checker/errors/__init__.py +0 -0
  12. guppylang_internals/checker/errors/comptime_errors.py +106 -0
  13. guppylang_internals/checker/errors/generic.py +45 -0
  14. guppylang_internals/checker/errors/linearity.py +300 -0
  15. guppylang_internals/checker/errors/type_errors.py +344 -0
  16. guppylang_internals/checker/errors/wasm.py +34 -0
  17. guppylang_internals/checker/expr_checker.py +1413 -0
  18. guppylang_internals/checker/func_checker.py +269 -0
  19. guppylang_internals/checker/linearity_checker.py +821 -0
  20. guppylang_internals/checker/stmt_checker.py +447 -0
  21. guppylang_internals/compiler/__init__.py +0 -0
  22. guppylang_internals/compiler/cfg_compiler.py +233 -0
  23. guppylang_internals/compiler/core.py +613 -0
  24. guppylang_internals/compiler/expr_compiler.py +989 -0
  25. guppylang_internals/compiler/func_compiler.py +97 -0
  26. guppylang_internals/compiler/hugr_extension.py +224 -0
  27. guppylang_internals/compiler/qtm_platform_extension.py +0 -0
  28. guppylang_internals/compiler/stmt_compiler.py +212 -0
  29. guppylang_internals/decorator.py +246 -0
  30. guppylang_internals/definition/__init__.py +0 -0
  31. guppylang_internals/definition/common.py +214 -0
  32. guppylang_internals/definition/const.py +74 -0
  33. guppylang_internals/definition/custom.py +492 -0
  34. guppylang_internals/definition/declaration.py +171 -0
  35. guppylang_internals/definition/extern.py +89 -0
  36. guppylang_internals/definition/function.py +302 -0
  37. guppylang_internals/definition/overloaded.py +150 -0
  38. guppylang_internals/definition/parameter.py +82 -0
  39. guppylang_internals/definition/pytket_circuits.py +405 -0
  40. guppylang_internals/definition/struct.py +392 -0
  41. guppylang_internals/definition/traced.py +151 -0
  42. guppylang_internals/definition/ty.py +51 -0
  43. guppylang_internals/definition/value.py +115 -0
  44. guppylang_internals/definition/wasm.py +61 -0
  45. guppylang_internals/diagnostic.py +523 -0
  46. guppylang_internals/dummy_decorator.py +76 -0
  47. guppylang_internals/engine.py +295 -0
  48. guppylang_internals/error.py +107 -0
  49. guppylang_internals/experimental.py +92 -0
  50. guppylang_internals/ipython_inspect.py +28 -0
  51. guppylang_internals/nodes.py +427 -0
  52. guppylang_internals/py.typed +0 -0
  53. guppylang_internals/span.py +150 -0
  54. guppylang_internals/std/__init__.py +0 -0
  55. guppylang_internals/std/_internal/__init__.py +0 -0
  56. guppylang_internals/std/_internal/checker.py +573 -0
  57. guppylang_internals/std/_internal/compiler/__init__.py +0 -0
  58. guppylang_internals/std/_internal/compiler/arithmetic.py +136 -0
  59. guppylang_internals/std/_internal/compiler/array.py +569 -0
  60. guppylang_internals/std/_internal/compiler/either.py +131 -0
  61. guppylang_internals/std/_internal/compiler/frozenarray.py +68 -0
  62. guppylang_internals/std/_internal/compiler/futures.py +30 -0
  63. guppylang_internals/std/_internal/compiler/list.py +348 -0
  64. guppylang_internals/std/_internal/compiler/mem.py +13 -0
  65. guppylang_internals/std/_internal/compiler/option.py +78 -0
  66. guppylang_internals/std/_internal/compiler/prelude.py +271 -0
  67. guppylang_internals/std/_internal/compiler/qsystem.py +48 -0
  68. guppylang_internals/std/_internal/compiler/quantum.py +118 -0
  69. guppylang_internals/std/_internal/compiler/tket_bool.py +55 -0
  70. guppylang_internals/std/_internal/compiler/tket_exts.py +59 -0
  71. guppylang_internals/std/_internal/compiler/wasm.py +135 -0
  72. guppylang_internals/std/_internal/compiler.py +0 -0
  73. guppylang_internals/std/_internal/debug.py +95 -0
  74. guppylang_internals/std/_internal/util.py +271 -0
  75. guppylang_internals/tracing/__init__.py +0 -0
  76. guppylang_internals/tracing/builtins_mock.py +62 -0
  77. guppylang_internals/tracing/frozenlist.py +57 -0
  78. guppylang_internals/tracing/function.py +186 -0
  79. guppylang_internals/tracing/object.py +551 -0
  80. guppylang_internals/tracing/state.py +69 -0
  81. guppylang_internals/tracing/unpacking.py +194 -0
  82. guppylang_internals/tracing/util.py +86 -0
  83. guppylang_internals/tys/__init__.py +0 -0
  84. guppylang_internals/tys/arg.py +115 -0
  85. guppylang_internals/tys/builtin.py +382 -0
  86. guppylang_internals/tys/common.py +110 -0
  87. guppylang_internals/tys/const.py +114 -0
  88. guppylang_internals/tys/errors.py +178 -0
  89. guppylang_internals/tys/param.py +251 -0
  90. guppylang_internals/tys/parsing.py +425 -0
  91. guppylang_internals/tys/printing.py +174 -0
  92. guppylang_internals/tys/subst.py +112 -0
  93. guppylang_internals/tys/ty.py +876 -0
  94. guppylang_internals/tys/var.py +49 -0
  95. guppylang_internals-0.21.0.dist-info/METADATA +253 -0
  96. guppylang_internals-0.21.0.dist-info/RECORD +98 -0
  97. guppylang_internals-0.21.0.dist-info/WHEEL +4 -0
  98. guppylang_internals-0.21.0.dist-info/licenses/LICENCE +201 -0
@@ -0,0 +1,613 @@
1
+ import itertools
2
+ from abc import ABC
3
+ from collections import defaultdict
4
+ from collections.abc import Callable, Iterator, Sequence
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING, Any, ClassVar, cast
8
+
9
+ import tket_exts
10
+ from hugr import Hugr, Node, Wire, ops
11
+ from hugr import tys as ht
12
+ from hugr.build import function as hf
13
+ from hugr.build.dfg import DP, DefinitionBuilder, DfBase
14
+ from hugr.hugr.base import OpVarCov
15
+ from hugr.hugr.node_port import ToNode
16
+ from hugr.std import PRELUDE
17
+ from typing_extensions import assert_never
18
+
19
+ from guppylang_internals.checker.core import (
20
+ FieldAccess,
21
+ Globals,
22
+ Place,
23
+ PlaceId,
24
+ TupleAccess,
25
+ Variable,
26
+ )
27
+ from guppylang_internals.definition.common import (
28
+ CheckedDef,
29
+ CompilableDef,
30
+ CompiledDef,
31
+ DefId,
32
+ MonomorphizableDef,
33
+ )
34
+ from guppylang_internals.definition.ty import TypeDef
35
+ from guppylang_internals.definition.value import CompiledCallableDef
36
+ from guppylang_internals.diagnostic import Error
37
+ from guppylang_internals.engine import ENGINE
38
+ from guppylang_internals.error import GuppyError, InternalGuppyError
39
+ from guppylang_internals.tys.arg import ConstArg, TypeArg
40
+ from guppylang_internals.tys.builtin import nat_type
41
+ from guppylang_internals.tys.common import ToHugrContext
42
+ from guppylang_internals.tys.const import BoundConstVar, ConstValue
43
+ from guppylang_internals.tys.param import ConstParam, Parameter, TypeParam
44
+ from guppylang_internals.tys.subst import Inst
45
+ from guppylang_internals.tys.ty import (
46
+ BoundTypeVar,
47
+ NumericType,
48
+ StructType,
49
+ TupleType,
50
+ Type,
51
+ )
52
+
53
+ if TYPE_CHECKING:
54
+ from guppylang_internals.tys.arg import Argument
55
+
56
+ CompiledLocals = dict[PlaceId, Wire]
57
+
58
+ #: Partial instantiation of generic type parameters for monomorphization.
59
+ #:
60
+ #: When compiling polymorphic definitions to Hugr, some of their generic parameters will
61
+ #: need to be monomorphized (e.g. to support language features that cannot be encoded
62
+ #: in Hugr, see `requires_monomorphization` for details). However, other generic
63
+ #: parameters can be faithfully captured in Hugr. Thus, we want to perform a *partial*
64
+ #: monomorphization when compiling to Hugr.
65
+ #:
66
+ #: A `mono_args: PartiallyMonomorphizedArgs` represents such a partial monomorphization
67
+ #: of generic parameters as a sequence of `Argument | None`. Concretely,
68
+ #: `mono_args[i] == None` means that the argument with index `i` is left as a
69
+ #: polymorphic Hugr type parameter, i.e. it is not monomorphised by Guppy. Otherwise,
70
+ #: `mono_args[i]` specifies the monomorphic instantiation for parameter `i`. For
71
+ #: example, a function with 3 generic parameters that should all be retained in Hugr
72
+ #: will have mono_args = `(None, None, None)`.
73
+ #:
74
+ #: Finally, note that this sequence is required to be a tuple to ensure hashability.
75
+ PartiallyMonomorphizedArgs = tuple["Argument | None", ...]
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class GlobalConstId:
80
+ id: int
81
+ base_name: str
82
+
83
+ _fresh_ids = itertools.count()
84
+
85
+ @staticmethod
86
+ def fresh(base_name: str) -> "GlobalConstId":
87
+ return GlobalConstId(next(GlobalConstId._fresh_ids), base_name)
88
+
89
+ @property
90
+ def name(self) -> str:
91
+ return f"{self.base_name}.{self.id}"
92
+
93
+
94
+ #: Unique identifier for a partially monomorphized definition.
95
+ #:
96
+ #: If the `DefId` corresponds to a `MonomorphizableDef`, then the second tuple entry
97
+ #: should hold the corresponding partial monomorphization. Otherwise, the second entry
98
+ #: should be `None`.
99
+ #:
100
+ #: Note the following subtlety: Functions are instances of `MonomorphizableDef`, even if
101
+ #: they aren't actually generic! This means that for non-generic function definitions we
102
+ #: will have an empty tuple `()` as `PartiallyMonomorphizedArgs`, but never `None`.
103
+ #: `None` only shows up for kinds of definitions that are never monomorphized (e.g.
104
+ #: definitions of constants).
105
+ MonoDefId = tuple[DefId, PartiallyMonomorphizedArgs | None]
106
+
107
+ #: Unique identifier for global Hugr constants and functions, the latter with optional
108
+ #: monomorphized type arguments
109
+ MonoGlobalConstId = tuple[GlobalConstId, PartiallyMonomorphizedArgs | None]
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class EntryMonomorphizeError(Error):
114
+ title: ClassVar[str] = "Invalid entry point"
115
+ span_label: ClassVar[str] = (
116
+ "Function `{name}` is not a valid compilation entry point since the value of "
117
+ "generic paramater `{param}` is not known"
118
+ )
119
+ name: str
120
+ param: Parameter
121
+
122
+
123
+ class CompilerContext(ToHugrContext):
124
+ """Compilation context containing all available definitions.
125
+
126
+ Maintains a `worklist` of definitions which have been used by other compiled code
127
+ (i.e. `compile_outer` has been called) but have not yet been compiled/lowered
128
+ themselves (i.e. `compile_inner` has not yet been called).
129
+ """
130
+
131
+ module: DefinitionBuilder[ops.Module]
132
+
133
+ #: The definitions compiled so far. For `MonomorphizableDef`s, their id can occur
134
+ #: multiple times here with respectively different partial monomorphizations. See
135
+ #: `MonoDefId` and `PartiallyMonomorphizedArgs` for details.
136
+ compiled: dict[MonoDefId, CompiledDef]
137
+
138
+ # use dict over set for deterministic iteration order
139
+ worklist: dict[MonoDefId, None]
140
+
141
+ global_funcs: dict[MonoGlobalConstId, hf.Function]
142
+
143
+ #: Partial instantiation for some of the type parameters of the current function for
144
+ #: the purpose of monomorphization
145
+ current_mono_args: PartiallyMonomorphizedArgs | None
146
+
147
+ checked_globals: Globals
148
+
149
+ def __init__(
150
+ self,
151
+ module: DefinitionBuilder[ops.Module],
152
+ ) -> None:
153
+ self.module = module
154
+ self.worklist = {}
155
+ self.compiled = {}
156
+ self.global_funcs = {}
157
+ self.checked_globals = Globals(None)
158
+ self.current_mono_args = None
159
+
160
+ @contextmanager
161
+ def set_monomorphized_args(
162
+ self, mono_args: PartiallyMonomorphizedArgs | None
163
+ ) -> Iterator[None]:
164
+ """Context manager to set the partial monomorphization for the function that is
165
+ compiled currently being compiled.
166
+ """
167
+ old = self.current_mono_args
168
+ self.current_mono_args = mono_args
169
+ yield
170
+ self.current_mono_args = old
171
+
172
+ def build_compiled_def(
173
+ self, def_id: DefId, type_args: Inst
174
+ ) -> tuple[CompiledDef, Inst]:
175
+ """Returns the compiled definitions corresponding to the given ID, along with
176
+ the remaining type args that have not been monomorphized away.
177
+
178
+ Might mutate the current Hugr if this definition has never been compiled before.
179
+ """
180
+ # TODO: The check below is a hack to support nested function definitions. We
181
+ # forgot to insert frames for nested functions into the DEF_STORE, which would
182
+ # make the call to `ENGINE.get_checked` below fail. For now, let's just short-
183
+ # cut if the function doesn't take any generic params (as is the case for all
184
+ # nested functions).
185
+ # See https://github.com/CQCL/guppylang/issues/1032
186
+ if (def_id, ()) in self.compiled:
187
+ assert type_args == []
188
+ return self.compiled[def_id, ()], type_args
189
+
190
+ mono_args: PartiallyMonomorphizedArgs | None = None
191
+ rem_args = type_args
192
+ compile_outer: Callable[[], CompiledDef]
193
+ match ENGINE.get_checked(def_id):
194
+ case MonomorphizableDef(params=params) as monomorphizable:
195
+ mono_args, rem_args = partially_monomorphize_args(
196
+ params, type_args, self
197
+ )
198
+ compile_outer = lambda: monomorphizable.monomorphize( # noqa: E731 (assign-lambda)
199
+ self.module, mono_args, self
200
+ )
201
+ case CompilableDef() as compilable:
202
+ compile_outer = lambda: compilable.compile_outer(self.module, self) # noqa: E731
203
+ case CompiledDef() as compiled_defn:
204
+ compile_outer = lambda: compiled_defn # noqa: E731
205
+ case defn:
206
+ compile_outer = assert_never(defn)
207
+ if (def_id, mono_args) not in self.compiled:
208
+ self.compiled[def_id, mono_args] = compile_outer()
209
+ self.worklist[def_id, mono_args] = None
210
+ return self.compiled[def_id, mono_args], rem_args
211
+
212
+ def compile(self, defn: CheckedDef) -> CompiledDef:
213
+ """Compiles the given definition and all of its dependencies into the current
214
+ Hugr."""
215
+ # Check and compile the entry point
216
+ entry_mono_args: PartiallyMonomorphizedArgs | None = None
217
+ entry_compiled: CompiledDef
218
+ match ENGINE.get_checked(defn.id):
219
+ case MonomorphizableDef(params=params) as defn:
220
+ # Entry point is not allowed to require monomorphization
221
+ for param in params:
222
+ if requires_monomorphization(param):
223
+ err = EntryMonomorphizeError(defn.defined_at, defn.name, param)
224
+ raise GuppyError(err)
225
+ # Thus, the partial monomorphization for the entry point is always empty
226
+ entry_mono_args = tuple(None for _ in params)
227
+ entry_compiled = defn.monomorphize(self.module, entry_mono_args, self)
228
+ case CompilableDef() as defn:
229
+ entry_compiled = defn.compile_outer(self.module, self)
230
+ case CompiledDef() as defn:
231
+ entry_compiled = defn
232
+ case defn:
233
+ entry_compiled = assert_never(defn)
234
+
235
+ self.compiled[defn.id, entry_mono_args] = entry_compiled
236
+ self.worklist[defn.id, entry_mono_args] = None
237
+
238
+ # Compile definition bodies
239
+ while self.worklist:
240
+ next_id, mono_args = self.worklist.popitem()[0]
241
+ next_def = self.compiled[next_id, mono_args]
242
+ with track_hugr_side_effects(), self.set_monomorphized_args(mono_args):
243
+ next_def.compile_inner(self)
244
+
245
+ return entry_compiled
246
+
247
+ def build_compiled_instance_func(
248
+ self,
249
+ ty: Type | TypeDef,
250
+ name: str,
251
+ type_args: Inst,
252
+ ) -> tuple[CompiledCallableDef, Inst] | None:
253
+ """Returns s compiled instance method along with its remaining type args that
254
+ have not been monomorphized away, or `None` if the type doesn't have a matching
255
+ method.
256
+
257
+ Compiles the definition and all of its dependencies into the current Hugr.
258
+ """
259
+ from guppylang_internals.engine import ENGINE
260
+
261
+ parsed_func = self.checked_globals.get_instance_func(ty, name)
262
+ if parsed_func is None:
263
+ return None
264
+ checked_func = ENGINE.get_checked(parsed_func.id)
265
+ compiled_func, rem_args = self.build_compiled_def(checked_func.id, type_args)
266
+ assert isinstance(compiled_func, CompiledCallableDef)
267
+ return compiled_func, rem_args
268
+
269
+ def declare_global_func(
270
+ self,
271
+ const_id: GlobalConstId,
272
+ func_ty: ht.PolyFuncType,
273
+ mono_args: PartiallyMonomorphizedArgs | None = None,
274
+ ) -> tuple[hf.Function, bool]:
275
+ """
276
+ Creates a function builder for a global function if it doesn't already exist,
277
+ else returns the existing one.
278
+ """
279
+ if (const_id, mono_args) in self.global_funcs:
280
+ return self.global_funcs[const_id, mono_args], True
281
+ func = self.module.module_root_builder().define_function(
282
+ name=const_id.name,
283
+ input_types=func_ty.body.input,
284
+ output_types=func_ty.body.output,
285
+ type_params=func_ty.params,
286
+ )
287
+ self.global_funcs[const_id, mono_args] = func
288
+ return func, False
289
+
290
+ def type_var_to_hugr(self, var: BoundTypeVar) -> ht.Type:
291
+ """Compiles a bound Guppy type variable into a Hugr type.
292
+
293
+ Takes care of performing partial monomorphization as specified in the current
294
+ context.
295
+ """
296
+ if self.current_mono_args is None:
297
+ # If we're not inside a monomorphized context, just return the Hugr
298
+ # variable with the same de Bruijn index
299
+ return ht.Variable(var.idx, var.hugr_bound)
300
+
301
+ match self.current_mono_args[var.idx]:
302
+ # Either we have a decided to monomorphize the corresponding parameter...
303
+ case TypeArg(ty=ty):
304
+ return ty.to_hugr(self)
305
+ # ... or we're still want to be generic in Hugr
306
+ case None:
307
+ # But in that case we'll have to down-shift the de Bruijn index to
308
+ # account for earlier params that have been monomorphized away
309
+ hugr_idx = compile_variable_idx(var.idx, self.current_mono_args)
310
+ return ht.Variable(hugr_idx, var.hugr_bound)
311
+ case _:
312
+ raise InternalGuppyError("Invalid monomorphization")
313
+
314
+ def const_var_to_hugr(self, var: BoundConstVar) -> ht.TypeArg:
315
+ """Compiles a bound Guppy constant variable into a Hugr type argument.
316
+
317
+ Takes care of performing partial monomorphization as specified in the current
318
+ context.
319
+ """
320
+ if var.ty != nat_type():
321
+ raise InternalGuppyError(
322
+ "Tried to convert non-nat const type argument to Hugr. This should "
323
+ "have been monomprhized away."
324
+ )
325
+ param = ht.BoundedNatParam(upper_bound=None)
326
+
327
+ if self.current_mono_args is None:
328
+ # If we're not inside a monomorphized context, just return the Hugr
329
+ # variable with the same de Bruijn index
330
+ return ht.VariableArg(var.idx, param)
331
+
332
+ match self.current_mono_args[var.idx]:
333
+ # Either we have a decided to monomorphize the corresponding parameter...
334
+ case ConstArg(const=ConstValue(value=int(v))):
335
+ return ht.BoundedNatArg(n=v)
336
+ # ... or we're still want to be generic in Hugr
337
+ case None:
338
+ # But in that case we'll have to down-shift the de Bruijn index to
339
+ # account for earlier params that have been monomorphized away
340
+ hugr_idx = compile_variable_idx(var.idx, self.current_mono_args)
341
+ return ht.VariableArg(hugr_idx, param)
342
+ case _:
343
+ raise InternalGuppyError("Invalid monomorphization")
344
+
345
+
346
+ @dataclass
347
+ class DFContainer:
348
+ """A dataflow graph under construction.
349
+
350
+ This class is passed through the entire compilation pipeline and stores a builder
351
+ for the dataflow child-graph currently being constructed as well as all live local
352
+ variables. Note that the variable map is mutated in-place and always reflects the
353
+ current compilation state.
354
+ """
355
+
356
+ builder: DfBase[ops.DfParentOp]
357
+ ctx: CompilerContext
358
+ locals: CompiledLocals = field(default_factory=dict)
359
+
360
+ def __init__(
361
+ self,
362
+ builder: DfBase[DP],
363
+ ctx: CompilerContext,
364
+ locals: CompiledLocals | None = None,
365
+ ) -> None:
366
+ generic_builder = cast(DfBase[ops.DfParentOp], builder)
367
+ if locals is None:
368
+ locals = {}
369
+ self.builder = generic_builder
370
+ self.ctx = ctx
371
+ self.locals = locals
372
+
373
+ def __getitem__(self, place: Place) -> Wire:
374
+ """Constructs a wire for a local place in this DFG.
375
+
376
+ Note that this mutates the Hugr since we might need to pack or unpack some
377
+ tuples to obtain a port for places that involve struct fields.
378
+ """
379
+ # First check, if we already have a wire for this place
380
+ if place.id in self.locals:
381
+ return self.locals[place.id]
382
+ # Otherwise, our only hope is that it's a struct or tuple value that we can
383
+ # rebuild by packing the wires of its constituting fields
384
+ elif isinstance(place.ty, StructType):
385
+ children: list[Place] = [
386
+ FieldAccess(place, field, None) for field in place.ty.fields
387
+ ]
388
+ elif isinstance(place.ty, TupleType):
389
+ children = [
390
+ TupleAccess(place, elem, idx, None)
391
+ for idx, elem in enumerate(place.ty.element_types)
392
+ ]
393
+ else:
394
+ raise InternalGuppyError(f"Couldn't obtain a port for `{place}`")
395
+ child_types = [child.ty.to_hugr(self.ctx) for child in children]
396
+ child_wires = [self[child] for child in children]
397
+ wire = self.builder.add_op(ops.MakeTuple(child_types), *child_wires)[0]
398
+ for child in children:
399
+ if child.ty.linear:
400
+ self.locals.pop(child.id)
401
+ self.locals[place.id] = wire
402
+ return wire
403
+
404
+ def __setitem__(self, place: Place, port: Wire) -> None:
405
+ # When assigning a struct value, we immediately unpack it recursively and only
406
+ # store the leaf wires.
407
+ is_return = isinstance(place, Variable) and is_return_var(place.name)
408
+ if isinstance(place.ty, StructType) and not is_return:
409
+ unpack = self.builder.add_op(
410
+ ops.UnpackTuple([t.ty.to_hugr(self.ctx) for t in place.ty.fields]), port
411
+ )
412
+ for field, field_port in zip(place.ty.fields, unpack, strict=True):
413
+ self[FieldAccess(place, field, None)] = field_port
414
+ # If we had a previous wire assigned to this place, we need forget about it.
415
+ # Otherwise, we might use this old value when looking up the place later
416
+ self.locals.pop(place.id, None)
417
+ # Same for tuples.
418
+ elif isinstance(place.ty, TupleType) and not is_return:
419
+ hugr_elem_tys = [ty.to_hugr(self.ctx) for ty in place.ty.element_types]
420
+ unpack = self.builder.add_op(ops.UnpackTuple(hugr_elem_tys), port)
421
+ for idx, (elem, elem_port) in enumerate(
422
+ zip(place.ty.element_types, unpack, strict=True)
423
+ ):
424
+ self[TupleAccess(place, elem, idx, None)] = elem_port
425
+ self.locals.pop(place.id, None)
426
+ else:
427
+ self.locals[place.id] = port
428
+
429
+ def __contains__(self, place: Place) -> bool:
430
+ return place.id in self.locals
431
+
432
+ def __copy__(self) -> "DFContainer":
433
+ # Make a copy of the var map so that mutating the copy doesn't
434
+ # mutate our variable mapping
435
+ return DFContainer(self.builder, self.ctx, self.locals.copy())
436
+
437
+
438
+ class CompilerBase(ABC):
439
+ """Base class for the Guppy compiler."""
440
+
441
+ ctx: CompilerContext
442
+
443
+ def __init__(self, ctx: CompilerContext) -> None:
444
+ self.ctx = ctx
445
+
446
+
447
+ def return_var(n: int) -> str:
448
+ """Name of the dummy variable for the n-th return value of a function.
449
+
450
+ During compilation, we treat return statements like assignments of dummy variables.
451
+ For example, the statement `return e0, e1, e2` is treated like `%ret0 = e0 ; %ret1 =
452
+ e1 ; %ret2 = e2`. This way, we can reuse our existing mechanism for passing of live
453
+ variables between basic blocks."""
454
+ return f"%ret{n}"
455
+
456
+
457
+ def is_return_var(x: str) -> bool:
458
+ """Checks whether the given name is a dummy return variable."""
459
+ return x.startswith("%ret")
460
+
461
+
462
+ def requires_monomorphization(param: Parameter) -> bool:
463
+ """Checks if a type parameter must be monomorphized before compiling to Hugr.
464
+
465
+ This is required for some Guppy language features that cannot be encoded in Hugr
466
+ yet. Currently, this only applies to non-nat const parameters.
467
+ """
468
+ match param:
469
+ case TypeParam():
470
+ return False
471
+ case ConstParam(ty=ty):
472
+ return ty != NumericType(NumericType.Kind.Nat)
473
+ case x:
474
+ return assert_never(x)
475
+
476
+
477
+ def partially_monomorphize_args(
478
+ params: Sequence[Parameter],
479
+ args: Inst,
480
+ ctx: CompilerContext,
481
+ ) -> tuple[PartiallyMonomorphizedArgs, Inst]:
482
+ """
483
+ Given an instantiation of all type parameters, extracts the ones that will need to
484
+ be monomorphized.
485
+
486
+ Also takes care of normalising bound variables w.r.t. the current monomorphization.
487
+ """
488
+ mono_args: list[Argument | None] = []
489
+ rem_args = []
490
+ for param, arg in zip(params, args, strict=True):
491
+ if requires_monomorphization(param):
492
+ # The constant could still refer to a bound variable, so we need to
493
+ # instantiate it w.r.t. to the current monomorphization
494
+ match arg:
495
+ case ConstArg(const=BoundConstVar(idx=idx)):
496
+ assert ctx.current_mono_args is not None
497
+ inst = ctx.current_mono_args[idx]
498
+ assert inst is not None
499
+ mono_args.append(inst)
500
+ case TypeArg():
501
+ # TODO: Once we also have type args that require monomorphization,
502
+ # we'll need to downshift de Bruijn indices here as well
503
+ raise NotImplementedError
504
+ case arg:
505
+ mono_args.append(arg)
506
+ else:
507
+ mono_args.append(None)
508
+ rem_args.append(arg)
509
+ return tuple(mono_args), rem_args
510
+
511
+
512
+ def compile_variable_idx(idx: int, mono_args: PartiallyMonomorphizedArgs) -> int:
513
+ """Returns the Hugr index for a variable.
514
+
515
+ Takes care of shifting down Guppy's indices to account for the current partial
516
+ monomorphization. This avoids gaps in Hugr indices due to the fact that not all
517
+ Guppy parameters are lowered to Hugr (some are monomorphized away).
518
+ """
519
+ assert mono_args[idx] is None, "Should not compile monomorphized index"
520
+ return sum(1 for arg in mono_args[:idx] if arg is None)
521
+
522
+
523
+ QUANTUM_EXTENSION = tket_exts.quantum()
524
+ RESULT_EXTENSION = tket_exts.result()
525
+ DEBUG_EXTENSION = tket_exts.debug()
526
+
527
+ #: List of extension ops that have side-effects, identified by their qualified name
528
+ EXTENSION_OPS_WITH_SIDE_EFFECTS: list[str] = [
529
+ # Results should be order w.r.t. each other but also w.r.t. panics
530
+ *(op_def.qualified_name() for op_def in RESULT_EXTENSION.operations.values()),
531
+ PRELUDE.get_op("panic").qualified_name(),
532
+ PRELUDE.get_op("exit").qualified_name(),
533
+ DEBUG_EXTENSION.get_op("StateResult").qualified_name(),
534
+ # Qubit allocation and deallocation have the side-effect of changing the number of
535
+ # available free qubits
536
+ QUANTUM_EXTENSION.get_op("QAlloc").qualified_name(),
537
+ QUANTUM_EXTENSION.get_op("QFree").qualified_name(),
538
+ QUANTUM_EXTENSION.get_op("MeasureFree").qualified_name(),
539
+ ]
540
+
541
+
542
+ def may_have_side_effect(op: ops.Op) -> bool:
543
+ """Checks whether an operation could have a side-effect.
544
+
545
+ We need to insert implicit state order edges between these kinds of nodes to ensure
546
+ they are executed in the correct order, even if there is no data dependency.
547
+ """
548
+ match op:
549
+ case ops.ExtOp() as ext_op:
550
+ return ext_op.op_def().qualified_name() in EXTENSION_OPS_WITH_SIDE_EFFECTS
551
+ case ops.Custom(op_name=op_name, extension=extension):
552
+ qualified_name = f"{extension}.{op_name}" if extension else op_name
553
+ return qualified_name in EXTENSION_OPS_WITH_SIDE_EFFECTS
554
+ case ops.Call() | ops.CallIndirect():
555
+ # Conservative choice is to assume that all calls could have side effects.
556
+ # In the future we could inspect the call graph to figure out a more
557
+ # precise answer
558
+ return True
559
+ case _:
560
+ return False
561
+
562
+
563
+ @contextmanager
564
+ def track_hugr_side_effects() -> Iterator[None]:
565
+ """Initialises the tracking of nodes with side-effects during Hugr building.
566
+
567
+ Ensures that state-order edges are implicitly inserted between side-effectful nodes
568
+ to ensure they are executed in the order they are added.
569
+ """
570
+ # Remember original `Hugr.add_node` method that is monkey-patched below.
571
+ hugr_add_node = Hugr.add_node
572
+ # Last node with potential side effects for each dataflow parent
573
+ prev_node_with_side_effect: defaultdict[Node, Node | None] = defaultdict(
574
+ lambda: None
575
+ )
576
+
577
+ def hugr_add_node_with_order(
578
+ self: Hugr[OpVarCov],
579
+ op: ops.Op,
580
+ parent: ToNode | None = None,
581
+ num_outs: int | None = None,
582
+ metadata: dict[str, Any] | None = None,
583
+ ) -> Node:
584
+ """Monkey-patched version of `Hugr.add_node` that takes care of implicitly
585
+ inserting state order edges between operations that could have side-effects.
586
+ """
587
+ new_node = hugr_add_node(self, op, parent, num_outs, metadata)
588
+ if may_have_side_effect(op):
589
+ handle_side_effect(new_node, self)
590
+ return new_node
591
+
592
+ def handle_side_effect(node: Node, hugr: Hugr[OpVarCov]) -> None:
593
+ """Performs the actual order-edge insertion, assuming that `node` has a side-
594
+ effect."""
595
+ parent = hugr[node].parent
596
+ if parent is not None:
597
+ if prev := prev_node_with_side_effect[parent]:
598
+ hugr.add_order_link(prev, node)
599
+ else:
600
+ # If this is the first side-effectful op in this DFG, make a recursive
601
+ # call with the parent since the parent is also considered side-
602
+ # effectful now. We shouldn't walk up through function definitions
603
+ # or basic blocks though
604
+ if not isinstance(hugr[parent].op, ops.FuncDefn | ops.DataflowBlock):
605
+ handle_side_effect(parent, hugr)
606
+ prev_node_with_side_effect[parent] = node
607
+
608
+ # Monkey-patch the `add_node` method
609
+ Hugr.add_node = hugr_add_node_with_order # type: ignore[method-assign]
610
+ try:
611
+ yield
612
+ finally:
613
+ Hugr.add_node = hugr_add_node # type: ignore[method-assign]