tket/serialize/pytket/encoder.rs
1//! Intermediate structure for encoding Hugrs into [`SerialCircuit`]s.
2
3mod unit_generator;
4mod unsupported_tracker;
5mod value_tracker;
6
7use hugr::core::HugrNode;
8use tket_json_rs::clexpr::InputClRegister;
9use tket_json_rs::clexpr::operator::{ClArgument, ClOperator, ClTerminal, ClVariable};
10use tket_json_rs::opbox::BoxID;
11pub use value_tracker::{
12 TrackedBit, TrackedParam, TrackedQubit, TrackedValue, TrackedValues, ValueTracker,
13};
14
15use hugr::ops::{OpTrait, OpType};
16use hugr::types::EdgeKind;
17
18use std::borrow::Cow;
19use std::collections::{HashMap, HashSet};
20use std::sync::{Arc, RwLock};
21
22use hugr::{Direction, HugrView, OutgoingPort, Wire};
23use itertools::Itertools;
24use tket_json_rs::circuit_json::{self, SerialCircuit};
25use unsupported_tracker::UnsupportedTracker;
26
27use super::opaque::OpaqueSubgraphs;
28use super::{PytketEncodeError, PytketEncodeOpError};
29use crate::metadata;
30use crate::serialize::pytket::circuit::{
31 AdditionalNodesAndWires, AdditionalSubgraph, EncodedCircuitInfo,
32};
33use crate::serialize::pytket::config::PytketEncoderConfig;
34use crate::serialize::pytket::extension::RegisterCount;
35use crate::serialize::pytket::opaque::{OpaqueSubgraph, OpaqueSubgraphPayload};
36
37/// The state of an in-progress [`SerialCircuit`] being built from a Hugr.
38#[derive(derive_more::Debug)]
39#[debug(bounds(H: HugrView))]
40pub struct PytketEncoderContext<H: HugrView> {
41 /// The name of the circuit being encoded.
42 name: Option<String>,
43 /// Global phase value.
44 ///
45 /// Defaults to "0" unless the circuit has a [METADATA_PHASE] metadata
46 /// entry.
47 phase: String,
48 /// The already-encoded serialised pytket commands.
49 commands: Vec<circuit_json::Command>,
50 /// A tracker for qubit/bit/parameter values associated with the circuit's wires.
51 ///
52 /// Contains methods to update the registers in the circuit being built.
53 pub values: ValueTracker<H::Node>,
54 /// A tracker for unsupported regions of the circuit.
55 unsupported: UnsupportedTracker<H::Node>,
56 /// A registry of already-encoded opaque subgraphs.
57 opaque_subgraphs: OpaqueSubgraphs<H::Node>,
58 /// Subgraphs in `opaque_subgraphs` that could not be emitted as opaque
59 /// barriers, and must be stored in the [`EncodedCircuitInfo`] instead when
60 /// finishing the encoding. Identified by their
61 /// [`super::opaque::SubgraphId`] in `opaque_subgraphs`.
62 non_emitted_subgraphs: Vec<AdditionalSubgraph>,
63 /// Configuration for the encoding.
64 ///
65 /// Contains custom operation/type/const emitters.
66 config: Arc<PytketEncoderConfig<H>>,
67 /// A cache of translated hugr functions, to be encoded as op boxes.
68 function_cache: Arc<RwLock<HashMap<H::Node, CachedEncodedFunction>>>,
69}
70
71/// Options used when emitting a pytket command from HUGR operations.
72///
73/// Mostly related to qubit/bit/parameter reuse.
74#[derive(Default)]
75#[expect(clippy::type_complexity)]
76pub struct EmitCommandOptions<'a> {
77 /// A function returning a list of input qubits to reuse in the output.
78 /// Any additional required qubits IDs will be freshly generated.
79 ///
80 /// If not provided, input qubits will be reused in the order they appear in the input.
81 reuse_qubits_fn: Option<Box<dyn FnOnce(&[TrackedQubit]) -> Vec<TrackedQubit> + 'a>>,
82 /// A function returning a list of input bits to reuse in the output.
83 /// Any additional required bits IDs will be freshly generated.
84 ///
85 /// If not provided, only fresh bit IDs will be used.
86 reuse_bits_fn: Option<Box<dyn FnOnce(&[TrackedBit]) -> Vec<TrackedBit> + 'a>>,
87 /// A function that computes the command's output parameters, given the
88 /// input expressions.
89 ///
90 /// If the number of parameters does not match the expected number, the
91 /// encoding result in an error.
92 ///
93 /// If not provided, no output parameters will be computed.
94 output_params_fn: Option<Box<dyn FnOnce(OutputParamArgs<'_>) -> Vec<String> + 'a>>,
95}
96
97impl<'a> EmitCommandOptions<'a> {
98 /// Create a new [`EmitCommandOptions`] with the default values.
99 pub fn new() -> Self {
100 Self {
101 reuse_qubits_fn: None,
102 reuse_bits_fn: None,
103 output_params_fn: None,
104 }
105 }
106
107 /// Set a function returning a list of input qubits to reuse in the output.
108 ///
109 /// Overrides the default behaviour of reusing input qubits in the order they appear in the input.
110 pub fn reuse_qubits(
111 mut self,
112 reuse_qubits: impl FnOnce(&[TrackedQubit]) -> Vec<TrackedQubit> + 'a,
113 ) -> Self {
114 self.reuse_qubits_fn = Some(Box::new(reuse_qubits));
115 self
116 }
117
118 /// Set a function returning a list of input bits to reuse in the output.
119 ///
120 /// Overrides the default behaviour of only using fresh bit IDs.
121 pub fn reuse_bits(
122 mut self,
123 reuse_bits: impl FnOnce(&[TrackedBit]) -> Vec<TrackedBit> + 'a,
124 ) -> Self {
125 self.reuse_bits_fn = Some(Box::new(reuse_bits));
126 self
127 }
128
129 /// Reuse all input bits in the output, in the order they appear in the input.
130 pub fn reuse_all_bits(self) -> Self {
131 self.reuse_bits(|inp_bits| inp_bits.to_owned())
132 }
133
134 /// Set a function that computes the command's output parameters, given the
135 /// input expressions.
136 ///
137 /// Overrides the default behaviour of not computing output parameters.
138 pub fn output_params(
139 mut self,
140 output_params: impl FnOnce(OutputParamArgs<'_>) -> Vec<String> + 'a,
141 ) -> Self {
142 self.output_params_fn = Some(Box::new(output_params));
143 self
144 }
145}
146
147impl<H: HugrView> PytketEncoderContext<H> {
148 /// Create a new [`PytketEncoderContext`] from a Hugr.
149 ///
150 /// # Arguments
151 ///
152 /// - `hugr`: The Hugr to encode.
153 /// - `region`: The region of the circuit to encode.
154 /// - `opaque_subgraphs`: The opaque subgraphs registry to use.
155 /// - `config`: The configuration for the encoder.
156 pub(super) fn new(
157 hugr: &H,
158 region: H::Node,
159 opaque_subgraphs: OpaqueSubgraphs<H::Node>,
160 config: impl Into<Arc<PytketEncoderConfig<H>>>,
161 ) -> Result<Self, PytketEncodeError<H::Node>> {
162 let config: Arc<PytketEncoderConfig<H>> = config.into();
163
164 // If the function name is empty, do not set the pytket circuit name.
165 let fn_name = match hugr.get_optype(region) {
166 OpType::FuncDefn(f) => Some(f.func_name()),
167 OpType::FuncDecl(f) => Some(f.func_name()),
168 _ => None,
169 }
170 .filter(|name| !name.is_empty())
171 .cloned();
172
173 // Backwards compatibility only: The global phase parameter
174 // of pytket circuits is now encoded as explicit `tket.global_phase` operations instead.
175 #[expect(deprecated)]
176 let phase = match hugr.get_metadata::<metadata::PytketPhaseExpr>(region) {
177 Some(p) => p.to_string(),
178 None => "0".to_string(),
179 };
180
181 Ok(Self {
182 name: fn_name,
183 phase,
184 commands: vec![],
185 values: ValueTracker::new(hugr, region, &config)?,
186 unsupported: UnsupportedTracker::new(hugr),
187 opaque_subgraphs,
188 non_emitted_subgraphs: vec![],
189 config,
190 function_cache: Arc::new(RwLock::new(HashMap::new())),
191 })
192 }
193
194 /// Traverse the circuit region in topological order, encoding the nodes as
195 /// pytket commands.
196 ///
197 /// Returns the final [`SerialCircuit`] if successful.
198 pub(super) fn run_encoder(
199 &mut self,
200 hugr: &H,
201 region: H::Node,
202 ) -> Result<(), PytketEncodeError<H::Node>> {
203 // When encoding a function, mark it as being encoded to detect recursive calls.
204 if hugr.get_parent(region) == Some(hugr.module_root()) {
205 let Ok(mut cache) = self.function_cache.write() else {
206 // If the cache is poisoned, some thread has panicked while holding the lock.
207 return Err(PytketEncodeError::custom("Detected encoder worker panic."));
208 };
209 cache.insert(region, CachedEncodedFunction::InEncodingStack);
210 }
211
212 let sg = hugr.scheduling_graph(region);
213 // TODO: Use weighted topological sort to try and explore unsupported
214 // ops first (that is, ops with no available emitter in `self.config`),
215 // to ensure we group them as much as possible.
216 let mut topo = petgraph::visit::Topo::new(sg.petgraph());
217 while let Some(pg_node) = topo.next(sg.petgraph()) {
218 let node = sg.pg_to_node(pg_node);
219 self.try_encode_node(node, hugr)?;
220 }
221 Ok(())
222 }
223
224 /// Finish building the pytket circuit
225 ///
226 /// # Returns
227 ///
228 /// * An [`EncodedCircuitInfo`] containing the final [`SerialCircuit`] and some additional metadata.
229 /// * The set of opaque subgraphs that were referenced (from/inside) pytket barriers.
230 #[expect(clippy::type_complexity)]
231 pub(super) fn finish(
232 mut self,
233 hugr: &H,
234 region: H::Node,
235 ) -> Result<(EncodedCircuitInfo, OpaqueSubgraphs<H::Node>), PytketEncodeError<H::Node>> {
236 // Add any remaining unsupported nodes
237 while !self.unsupported.is_empty() {
238 let node = self.unsupported.iter().next().unwrap();
239 let opaque_subgraphs = self.unsupported.extract_component(node, hugr)?;
240 self.emit_unsupported(&opaque_subgraphs, hugr)?;
241 }
242
243 let tracker_result = self.values.finish(hugr, region)?;
244
245 let mut ser = SerialCircuit::new(self.name, self.phase);
246
247 ser.commands = self.commands;
248 ser.qubits = tracker_result.qubits.into_iter().map_into().collect();
249 ser.bits = tracker_result.bits.into_iter().map_into().collect();
250 ser.implicit_permutation = tracker_result.qubit_permutation;
251 ser.number_of_ws = None;
252
253 let info = EncodedCircuitInfo {
254 serial_circuit: ser,
255 input_params: tracker_result.input_params,
256 output_params: tracker_result.params,
257 additional_nodes_and_wires: AdditionalNodesAndWires {
258 additional_subgraphs: self.non_emitted_subgraphs,
259 straight_through_wires: tracker_result.straight_through_wires,
260 },
261 output_qubits: tracker_result.qubit_outputs,
262 output_bits: tracker_result.bit_outputs,
263 };
264
265 Ok((info, self.opaque_subgraphs))
266 }
267
268 /// Returns a reference to this encoder's configuration.
269 pub fn config(&self) -> &PytketEncoderConfig<H> {
270 &self.config
271 }
272
273 /// Returns the values associated with a wire.
274 ///
275 /// Marks the port connection as explored. When all ports connected to the
276 /// wire have been explored, the wire is removed from the tracker.
277 ///
278 /// If the input wire is the output of an unsupported node, a subgraph of
279 /// unsupported nodes containing it will be emitted as a pytket barrier.
280 ///
281 /// This function SHOULD NOT be called before determining that the target
282 /// operation is supported, as it will mark the wire as explored and
283 /// potentially remove it from the tracker. To determine if a wire type is
284 /// supported, use [`PytketEncoderConfig::type_to_pytket`] on the encoder
285 /// context's [`PytketEncoderContext::config`].
286 ///
287 /// ### Errors
288 ///
289 /// - [`PytketEncodeOpError::WireHasNoValues`] if the wire is not tracked or
290 /// has a type that cannot be converted to pytket values.
291 pub fn get_wire_values(
292 &mut self,
293 wire: Wire<H::Node>,
294 hugr: &H,
295 ) -> Result<Cow<'_, [TrackedValue]>, PytketEncodeError<H::Node>> {
296 if self.values.peek_wire_values(wire).is_some() {
297 return Ok(self.values.wire_values(wire).unwrap());
298 }
299
300 // If the wire values have not been registered yet, it may be because
301 // the wire is the output of an unsupported node group.
302 //
303 // We need to emit the unsupported node here before returning the values.
304 if self.unsupported.is_unsupported(wire.node()) {
305 let unsupported_nodes = self.unsupported.extract_component(wire.node(), hugr)?;
306 self.emit_unsupported(&unsupported_nodes, hugr)?;
307 debug_assert!(!self.unsupported.is_unsupported(wire.node()));
308 return self.get_wire_values(wire, hugr);
309 }
310
311 Err(PytketEncodeOpError::WireHasNoValues { wire }.into())
312 }
313
314 /// Peek values associated with a wire without marking the connection as explored.
315 pub fn peek_wire_values(&self, wire: Wire<H::Node>) -> Option<&[TrackedValue]> {
316 self.values.peek_wire_values(wire)
317 }
318
319 /// Given a node in the HUGR, returns all the [`TrackedValue`]s associated
320 /// with its inputs.
321 ///
322 /// These values can be used with the [`PytketEncoderContext::values`]
323 /// tracker to retrieve the corresponding pytket registers and parameter
324 /// expressions. See [`ValueTracker::qubit_register`],
325 /// [`ValueTracker::bit_register`], and [`ValueTracker::param_expression`].
326 ///
327 /// Marks the input connections to the node as explored. When all ports
328 /// connected to a wire have been explored, the wire is removed from the
329 /// tracker.
330 ///
331 /// If an input wire is the output of an unsupported node, a subgraph of
332 /// unsupported nodes containing it will be emitted as a pytket barrier.
333 ///
334 /// This function SHOULD NOT be called before determining that the node
335 /// operation is supported, as it will mark the input connections as explored
336 /// and potentially remove them from the tracker. To determine if a node
337 /// operation is supported, use [`PytketEncoderConfig::type_to_pytket`] on
338 /// the encoder context's [`PytketEncoderContext::config`].
339 pub fn get_input_values(
340 &mut self,
341 node: H::Node,
342 hugr: &H,
343 ) -> Result<TrackedValues, PytketEncodeError<H::Node>> {
344 self.get_input_values_internal(node, hugr, |_| true)?
345 .try_into_tracked_values()
346 }
347
348 /// Auxiliary function used to collect the input values of a node.
349 /// See [`PytketEncoderContext::get_input_values`].
350 ///
351 /// Given a node in the HUGR, returns all the [`TrackedValue`]s associated
352 /// with its inputs. Calls `wire_filter` to decide which incoming wires to include.
353 fn get_input_values_internal(
354 &mut self,
355 node: H::Node,
356 hugr: &H,
357 wire_filter: impl Fn(Wire<H::Node>) -> bool,
358 ) -> Result<NodeInputValues<H::Node>, PytketEncodeError<H::Node>> {
359 let mut tracked_values = TrackedValues::default();
360 let mut unknown_values = Vec::new();
361
362 let optype = hugr.get_optype(node);
363 let other_input_port = optype.other_input_port();
364 for input in hugr.node_inputs(node) {
365 // Ignore order edges.
366 if Some(input) == other_input_port {
367 continue;
368 }
369 // Dataflow ports should have a single linked neighbour.
370 let (neigh, neigh_out) = hugr
371 .single_linked_output(node, input)
372 .expect("Dataflow input port should have a single neighbour");
373 let wire = Wire::new(neigh, neigh_out);
374 if !wire_filter(wire) {
375 continue;
376 }
377
378 match self.get_wire_values(wire, hugr) {
379 Ok(values) => tracked_values.extend(values.iter().copied()),
380 Err(PytketEncodeError::OpEncoding(PytketEncodeOpError::WireHasNoValues {
381 wire,
382 })) => unknown_values.push(wire),
383 Err(e) => return Err(e),
384 }
385 }
386 Ok(NodeInputValues {
387 tracked_values,
388 unknown_values,
389 })
390 }
391
392 /// Helper to emit a new tket1 command corresponding to a single HUGR node.
393 ///
394 /// See [`EmitCommandOptions`] for controlling the output qubit, bits, and
395 /// parameter expressions.
396 ///
397 /// See [`PytketEncoderContext::emit_command`] for more general cases where
398 /// commands are not associated to a specific node.
399 ///
400 /// ## Arguments
401 ///
402 /// - `pytket_optype`: The tket1 operation type to emit.
403 /// - `node`: The HUGR node for which to emit the command. Qubits and bits
404 /// are automatically retrieved from the node's inputs/outputs.
405 /// - `circ`: The circuit containing the node.
406 /// - `options`: Options for controlling the output qubit, bits, and
407 /// parameter expressions.
408 pub fn emit_node(
409 &mut self,
410 pytket_optype: tket_json_rs::OpType,
411 node: H::Node,
412 hugr: &H,
413 options: EmitCommandOptions,
414 ) -> Result<(), PytketEncodeError<H::Node>> {
415 self.emit_node_command(node, hugr, options, move |inputs| {
416 make_tk1_operation(pytket_optype, inputs)
417 })
418 }
419
420 /// Helper to emit a new tket1 command corresponding to a single HUGR node,
421 /// using a custom operation generator and computing output parameter
422 /// expressions. Use [`PytketEncoderContext::emit_node`] when pytket operation
423 /// can be defined directly from a [`tket_json_rs::OpType`].
424 ///
425 /// See [`PytketEncoderContext::emit_command`] for a general case emitter.
426 ///
427 /// ## Arguments
428 ///
429 /// - `node`: The HUGR node for which to emit the command. Qubits and bits
430 /// are automatically retrieved from the node's inputs/outputs. Input
431 /// arguments are listed in order, followed by output-only args.
432 /// - `circ`: The circuit containing the node.
433 /// - `reuse_bits`: A function returning a lits of input bits to reuse in the output.
434 /// Any additional required bits IDs will be freshly generated.
435 /// - `output_params`: A function that computes the output parameter
436 /// expressions from the list of input parameters. If the number of
437 /// parameters does not match the expected number, the encoding will fail.
438 /// - `make_operation`: A function that takes the number of qubits, bits,
439 /// and the list of input parameter expressions and returns a pytket
440 /// operation. See [`make_tk1_operation`] for a helper function to create
441 /// it.
442 pub fn emit_node_command(
443 &mut self,
444 node: H::Node,
445 hugr: &H,
446 options: EmitCommandOptions,
447 make_operation: impl FnOnce(MakeOperationArgs<'_>) -> circuit_json::Operation,
448 ) -> Result<(), PytketEncodeError<H::Node>> {
449 let TrackedValues {
450 mut qubits,
451 mut bits,
452 params,
453 } = self.get_input_values(node, hugr)?;
454 let params: Vec<String> = params
455 .into_iter()
456 .map(|p| self.values.param_expression(p).to_owned())
457 .collect();
458
459 // Update the values in the node's outputs.
460 //
461 // We preserve the order of linear values in the input.
462 let new_outputs =
463 self.register_node_outputs(node, hugr, &qubits, &bits, ¶ms, options, |_| true)?;
464 qubits.extend(new_outputs.qubits);
465 bits.extend(new_outputs.bits);
466
467 // Preserve the pytket opgroup, if it got stored in the metadata.
468 let opgroup: Option<String> = hugr
469 .get_metadata::<metadata::PytketOpGroup>(node)
470 .map(ToString::to_string);
471
472 let args = MakeOperationArgs {
473 num_qubits: qubits.len(),
474 num_bits: bits.len(),
475 params: Cow::Borrowed(¶ms),
476 };
477 let op = make_operation(args);
478
479 self.emit_command(op, &qubits, &bits, opgroup);
480 Ok(())
481 }
482
483 /// Helper to emit a node that transparently forwards its inputs to its
484 /// outputs, resulting in no pytket operation.
485 ///
486 /// It registers the node's input qubits and bits to the output
487 /// wires, without modifying the tket1 circuit being constructed.
488 /// Output parameters are more flexible, and output expressions can be
489 /// computed from the input parameters via the `output_params` function.
490 ///
491 /// The node's inputs should have exactly the same number of qubits and
492 /// bits. This method will return an error if that is not the case.
493 ///
494 /// You must also ensure that all input and output types are supported by
495 /// the encoder. Otherwise, the function will return an error.
496 ///
497 /// ## Arguments
498 ///
499 /// - `node`: The HUGR node for which to emit the command. Qubits and bits are
500 /// automatically retrieved from the node's inputs/outputs.
501 /// - `circ`: The circuit containing the node.
502 /// - `output_params`: A function that computes the output parameter
503 /// expressions from the list of input parameters. If the number of parameters
504 /// does not match the expected number, the encoding will fail.
505 pub fn emit_transparent_node(
506 &mut self,
507 node: H::Node,
508 hugr: &H,
509 output_params: impl FnOnce(OutputParamArgs<'_>) -> Vec<String>,
510 ) -> Result<(), PytketEncodeError<H::Node>> {
511 let input_values = self.get_input_values(node, hugr)?;
512 let output_counts = self.node_output_values(node, hugr)?;
513 let total_out_count: RegisterCount = output_counts.iter().map(|(_, c)| *c).sum();
514
515 // Compute all the output parameters at once
516 let input_params: Vec<String> = input_values
517 .params
518 .into_iter()
519 .map(|p| self.values.param_expression(p).to_owned())
520 .collect_vec();
521 let out_params = output_params(OutputParamArgs {
522 expected_count: total_out_count.params,
523 input_params: &input_params,
524 });
525
526 // Check that we got the expected number of outputs.
527 if input_values.qubits.len() != total_out_count.qubits {
528 return Err(PytketEncodeError::custom(format!(
529 "Mismatched number of input and output qubits while trying to emit a transparent operation for {}. We have {} inputs but {} outputs.",
530 hugr.get_optype(node),
531 input_values.qubits.len(),
532 total_out_count.qubits,
533 )));
534 }
535 if input_values.bits.len() != total_out_count.bits {
536 return Err(PytketEncodeError::custom(format!(
537 "Mismatched number of input and output bits while trying to emit a transparent operation for {}. We have {} inputs but {} outputs.",
538 hugr.get_optype(node),
539 input_values.bits.len(),
540 total_out_count.bits,
541 )));
542 }
543 if out_params.len() != total_out_count.params {
544 return Err(PytketEncodeError::custom(format!(
545 "Expected {} parameters in the input values for a {}, but got {}.",
546 total_out_count.params,
547 hugr.get_optype(node),
548 out_params.len()
549 )));
550 }
551
552 // Now we can gather all inputs and assign them to the node outputs transparently.
553 let mut qubits = input_values.qubits.into_iter();
554 let mut bits = input_values.bits.into_iter();
555 let mut params = out_params.into_iter();
556 for (wire, count) in output_counts {
557 let mut values: Vec<TrackedValue> = Vec::with_capacity(count.total());
558 values.extend(qubits.by_ref().take(count.qubits).map(TrackedValue::Qubit));
559 values.extend(bits.by_ref().take(count.bits).map(TrackedValue::Bit));
560 for p in params.by_ref().take(count.params) {
561 values.push(self.values.new_param(p).into());
562 }
563 self.values.register_wire(wire, values, hugr)?;
564 }
565
566 Ok(())
567 }
568
569 /// Helper to emit a new tket1 command corresponding to subgraph of unsupported nodes,
570 /// encoded inside a pytket barrier.
571 ///
572 /// ## Arguments
573 ///
574 /// - `subgraph`: The subgraph of unsupported nodes to encode as an opaque subgraph.
575 /// - `circ`: The circuit containing the unsupported nodes.
576 fn emit_unsupported(
577 &mut self,
578 subgraph: &OpaqueSubgraph<H::Node>,
579 hugr: &H,
580 ) -> Result<(), PytketEncodeError<H::Node>> {
581 // Encode a payload referencing the subgraph in the Hugr.
582 let subgraph_id = self
583 .opaque_subgraphs
584 .register_opaque_subgraph(subgraph.clone());
585
586 // Collects the input values for the subgraph.
587 //
588 // The [`UnsupportedTracker`] ensures that at this point all local input wires must come from
589 // already-encoded nodes, and not from other unsupported nodes not in `unsupported_nodes`.
590 let mut op_values = TrackedValues::default();
591 for (node, port) in subgraph.incoming_ports().iter() {
592 let (neigh, neigh_out) = hugr
593 .single_linked_output(*node, *port)
594 .expect("Dataflow input port should have a single neighbour");
595 let wire = Wire::new(neigh, neigh_out);
596
597 let Ok(tracked_values) = self.get_wire_values(wire, hugr) else {
598 // If the wire is not tracked, no need to consume it.
599 continue;
600 };
601 op_values.extend(tracked_values.iter().cloned());
602 }
603
604 let input_param_exprs: Vec<String> = std::mem::take(&mut op_values.params)
605 .into_iter()
606 .map(|p| self.values.param_expression(p).to_owned())
607 .collect();
608
609 let payload = OpaqueSubgraphPayload::new_external(subgraph_id, input_param_exprs.clone());
610
611 // Update the values in the node's outputs, and extend `op_values` with
612 // any new output values.
613 //
614 // Output parameters are mapped to a fresh variable, that can be tracked
615 // back to the encoded subcircuit's function name.
616 let mut out_param_count = 0;
617 let input_qubits = op_values.qubits.clone();
618 let input_bits = op_values.bits.clone();
619 let mut out_qubits = input_qubits.as_slice();
620 let mut out_bits = input_bits.as_slice();
621
622 for ((out_node, out_port), ty) in subgraph
623 .outgoing_ports()
624 .iter()
625 .zip(subgraph.signature().output().iter())
626 {
627 if self.config().type_to_pytket(ty).is_none() {
628 // Do not try to register ports with unsupported types.
629 continue;
630 }
631 let new_outputs = self.register_port_output(
632 *out_node,
633 *out_port,
634 hugr,
635 &mut out_qubits,
636 &mut out_bits,
637 &input_param_exprs,
638 |p| {
639 let range = out_param_count..out_param_count + p.expected_count;
640 out_param_count += p.expected_count;
641 range.map(|i| subgraph_id.output_parameter(i)).collect_vec()
642 },
643 )?;
644 op_values.append(new_outputs);
645 }
646
647 // Check that we have qubits or bits to attach the barrier command to.
648 if op_values.qubits.is_empty() && op_values.bits.is_empty() {
649 // We cannot associate this subgraph to any qubit or bit register in
650 // the pytket circuit, so we'll store it in the
651 // [`AdditionalSubgraph`]s instead when finishing the encoding.
652 //
653 // That list contains a list of subgraphs so we don't need to do any
654 // additional handling, but if we preferred in the future we could
655 // instead merge a single list of nodes if we wanted.
656 self.non_emitted_subgraphs.push(AdditionalSubgraph {
657 id: subgraph_id,
658 params: input_param_exprs.clone(),
659 });
660 } else {
661 // If there are registers to which to attach, emit it as a barrier command.
662
663 // Create the pytket operation, with an external reference to the subgraph.
664 let args = MakeOperationArgs {
665 num_qubits: op_values.qubits.len(),
666 num_bits: op_values.bits.len(),
667 params: Cow::Borrowed(&[]),
668 };
669 let mut pytket_op = make_tk1_operation(tket_json_rs::OpType::Barrier, args);
670 pytket_op.data = Some(serde_json::to_string(&payload).unwrap());
671
672 self.emit_command(pytket_op, &op_values.qubits, &op_values.bits, None);
673 }
674
675 Ok(())
676 }
677
678 /// Emit a new tket1 command.
679 ///
680 /// This is a general-purpose command that can be used to emit any tket1
681 /// operation, not necessarily corresponding to a specific HUGR node.
682 ///
683 /// Ensure that any output wires from the node being processed gets the
684 /// appropriate values registered by calling [`ValueTracker::register_wire`]
685 /// on the context's [`PytketEncoderContext::values`] tracker.
686 ///
687 /// In general you should prefer using [`PytketEncoderContext::emit_node`]
688 /// as it automatically computes the input qubits and bits from the HUGR
689 /// node, and ensure that output wires get their new values registered on
690 /// the tracker.
691 ///
692 /// ## Arguments
693 ///
694 /// - `pytket_op`: The tket1 operation to emit. See [`make_tk1_operation`]
695 /// for a helper function to create it.
696 /// - `qubits`: The qubit registers to use as inputs/outputs of the pytket
697 /// op. Normally obtained from a HUGR node's inputs using
698 /// [`PytketEncoderContext::get_input_values`] or allocated via
699 /// [`ValueTracker::new_qubit`].
700 /// - `bits`: The bit registers to use as inputs/outputs of the pytket op.
701 /// Normally obtained from a HUGR node's inputs using
702 /// [`PytketEncoderContext::get_input_values`] or allocated via
703 /// [`ValueTracker::new_bit`].
704 /// - `opgroup`: A tket1 [operation group
705 /// identifier](https://docs.quantinuum.com/tket/user-guide/manual/manual_circuit.html#modifying-operations-within-circuits),
706 /// if any.
707 pub fn emit_command(
708 &mut self,
709 pytket_op: circuit_json::Operation,
710 qubits: &[TrackedQubit],
711 bits: &[TrackedBit],
712 opgroup: Option<String>,
713 ) {
714 let qubit_regs = qubits.iter().map(|&qb| self.values.qubit_register(qb));
715 let bit_regs = bits.iter().map(|&b| self.values.bit_register(b));
716 let command = circuit_json::Command {
717 op: pytket_op,
718 args: qubit_regs.chain(bit_regs).cloned().collect(),
719 opgroup,
720 };
721
722 self.commands.push(command);
723 }
724
725 /// Helper to emit a `CircBox` tket1 command corresponding to a region of the Hugr.
726 ///
727 /// Returns a bool indicating whether the subcircuit was successfully emitted,
728 /// or should be encoded opaquely instead. This is the case when the subcircuit
729 /// contains output parameters.
730 ///
731 // TODO: Support output parameters in subcircuits. This may require
732 // substituting variables in the parameter expressions.
733 #[expect(unused)]
734 fn emit_subcircuit(
735 &mut self,
736 node: H::Node,
737 hugr: &H,
738 ) -> Result<EncodeStatus, PytketEncodeError<H::Node>> {
739 let config = Arc::clone(&self.config);
740
741 // Recursively encode the sub-graph.
742 let opaque_subgraphs = std::mem::take(&mut self.opaque_subgraphs);
743 let mut subencoder = PytketEncoderContext::new(hugr, node, opaque_subgraphs, config)?;
744 subencoder.function_cache = self.function_cache.clone();
745 subencoder.run_encoder(hugr, node)?;
746
747 let (info, opaque_subgraphs) = subencoder.finish(hugr, node)?;
748 if !info.output_params.is_empty() {
749 return Ok(EncodeStatus::Unsupported);
750 }
751 self.opaque_subgraphs = opaque_subgraphs;
752
753 self.emit_circ_box(node, info.serial_circuit, hugr)?;
754 Ok(EncodeStatus::Success)
755 }
756
757 /// Helper to emit a `CircBox` tket1 command corresponding to a function definition in the Hugr.
758 ///
759 /// The function encoding is cached and reused if possible.
760 ///
761 /// Returns a bool indicating whether the subcircuit was successfully emitted,
762 /// or should be encoded opaquely instead. This is the case when the subcircuit
763 /// contains output parameters.
764 ///
765 // TODO: Support output parameters in subcircuits. This may require
766 // substituting variables in the parameter expressions.
767 #[expect(unused)]
768 fn emit_function_call(
769 &mut self,
770 node: H::Node,
771 function: H::Node,
772 hugr: &H,
773 ) -> Result<EncodeStatus, PytketEncodeError<H::Node>> {
774 let cache = self.function_cache.read().ok();
775 if let Some(encoded) = cache.as_ref().and_then(|c| c.get(&function)) {
776 let encoded = encoded.clone();
777 drop(cache);
778 match encoded {
779 CachedEncodedFunction::Encoded { serial_circuit } => {
780 self.emit_circ_box(node, serial_circuit, hugr)?;
781 return Ok(EncodeStatus::Success);
782 }
783 CachedEncodedFunction::Unsupported | CachedEncodedFunction::InEncodingStack => {
784 return Ok(EncodeStatus::Unsupported);
785 }
786 };
787 }
788 drop(cache);
789
790 // If the function is not cached, we need to encode it.
791 let config = Arc::clone(&self.config);
792 let opaque_subgraphs = std::mem::take(&mut self.opaque_subgraphs);
793 // Recursively encode the sub-graph.
794 let mut subencoder = PytketEncoderContext::new(hugr, function, opaque_subgraphs, config)?;
795 subencoder.function_cache = self.function_cache.clone();
796 subencoder.run_encoder(hugr, function)?;
797 let (info, opaque_subgraphs) = subencoder.finish(hugr, function)?;
798 self.opaque_subgraphs = opaque_subgraphs;
799
800 let (result, cached_fn) = match info.output_params.is_empty() {
801 true => (
802 EncodeStatus::Success,
803 CachedEncodedFunction::Encoded {
804 serial_circuit: info.serial_circuit.clone(),
805 },
806 ),
807 false => (
808 EncodeStatus::Unsupported,
809 CachedEncodedFunction::Unsupported,
810 ),
811 };
812
813 // Cache the encoded subcircuit for future use.
814 // If the cache is poisoned, ignore it.
815 if let Ok(mut cache) = self.function_cache.write() {
816 cache.insert(function, cached_fn);
817 }
818
819 if result == EncodeStatus::Success {
820 self.emit_circ_box(node, info.serial_circuit, hugr)?;
821 }
822 Ok(result)
823 }
824
825 /// Helper to emit a `CircBox` tket1 command from a Serialised circuit.
826 fn emit_circ_box(
827 &mut self,
828 node: H::Node,
829 boxed_circuit: SerialCircuit,
830 hugr: &H,
831 ) -> Result<(), PytketEncodeError<H::Node>> {
832 self.emit_node_command(
833 node,
834 hugr,
835 EmitCommandOptions::new().reuse_all_bits(),
836 |args| {
837 let mut pytket_op = make_tk1_operation(tket_json_rs::OpType::CircBox, args);
838 pytket_op.op_box = Some(tket_json_rs::opbox::OpBox::CircBox {
839 id: BoxID::new(),
840 circuit: boxed_circuit,
841 });
842 pytket_op
843 },
844 )?;
845 Ok(())
846 }
847
848 /// Encode a single circuit node into pytket commands and update the
849 /// encoder.
850 ///
851 /// Dispatches to the registered encoders, trying each in turn until one
852 /// successfully encodes the operation.
853 ///
854 /// Returns `true` if the node was successfully encoded, or `false` if none
855 /// of the encoders could process it and the node got added to the
856 /// unsupported set.
857 fn try_encode_node(
858 &mut self,
859 node: H::Node,
860 hugr: &H,
861 ) -> Result<EncodeStatus, PytketEncodeError<H::Node>> {
862 let optype = hugr.get_optype(node);
863
864 // Try to register non-local inputs to nodes when possible (e.g.
865 // constants, function definitions).
866 //
867 // Otherwise, mark the node as unsupported.
868 if self.encode_nonlocal_inputs(node, optype, hugr)? == EncodeStatus::Unsupported {
869 self.unsupported.record_node(node, hugr);
870 return Ok(EncodeStatus::Unsupported);
871 }
872
873 // Try to encode the operation using each of the registered encoders.
874 //
875 // If none of the encoders can handle the operation, we just add it to
876 // the unsupported tracker and move on.
877 match optype {
878 OpType::ExtensionOp(op)
879 // Ignore nodes with order edges, as they cannot be represented in the pytket circuit.
880 if !self.has_order_edges(node, optype, hugr) => {
881 let config = Arc::clone(&self.config);
882 if config.op_to_pytket(node, op, hugr, self)? == EncodeStatus::Success {
883 return Ok(EncodeStatus::Success);
884 }
885 }
886 OpType::LoadConstant(constant)
887 // If we are loading a supported type, emit a transparent node
888 // by reassigning the input values to the new outputs.
889 //
890 // Otherwise, if we're loading an unsupported type, this node
891 // should be part of an unsupported subgraph.
892 if self
893 .config()
894 .type_to_pytket(constant.constant_type())
895 .is_some()
896 => {
897 let (const_node, const_port) = hugr
898 .single_linked_output(node, constant.constant_port())
899 .expect("LoadConstant node must have a constant input");
900 let const_wire = Wire::new(const_node, const_port);
901 // Loading a constant is transparent only when the constant
902 // value is supported an has already been registered.
903 // Otherwise the load belongs to the same opaque subgraph as
904 // its unsupported constant.
905 if self.values.peek_wire_values(const_wire).is_some() {
906 self.emit_transparent_node(node, hugr, |ps| ps.input_params.to_owned())?;
907 return Ok(EncodeStatus::Success);
908 }
909 }
910 OpType::Const(op) => {
911 let config = Arc::clone(&self.config);
912 if self.config().type_to_pytket(&op.get_type()).is_some()
913 && let Some(values) = config.const_to_pytket(&op.value, self)?
914 {
915 let wire = Wire::new(node, 0);
916 self.values.register_wire(wire, values, hugr)?;
917 return Ok(EncodeStatus::Success);
918 }
919 }
920 // TODO: DFG and function call emissions are temporarily disabled,
921 // since we cannot track additional metadata associated with the
922 // nested circuit in a `CircuitBox` as we'd do for the root one in
923 // [`EncodedCircuitInfo`].
924 //
925 // See the `unsupported_extras_in_circ_box` case in
926 // `tests::encoded_circuit_roundtrip` for a failing case when this
927 // is enabled.
928 /*
929 OpType::DFG(_) => return self.emit_subcircuit(node, circ),
930 OpType::Call(call) => {
931 let (fn_node, _) = hugr
932 .single_linked_output(node, call.called_function_port())
933 .expect("Function call must be linked to a function");
934 if hugr.get_optype(fn_node).is_func_defn()
935 && self.emit_function_call(node, fn_node, hugr)? == EncodeStatus::Success
936 {
937 return Ok(EncodeStatus::Success);
938 }
939 }
940 */
941 OpType::Input(_) | OpType::Output(_) => {
942 // I/O nodes are handled by the container's encoder.
943 return Ok(EncodeStatus::Success);
944 }
945 _ => {}
946 }
947
948 self.unsupported.record_node(node, hugr);
949 Ok(EncodeStatus::Unsupported)
950 }
951
952 /// The toposort traversal in `run_encoder` only explores nodes in the
953 /// region.
954 ///
955 /// When a node has a non-local input, we must process its originating node
956 /// before trying to translate it.
957 ///
958 /// In general if a node has a non-local dataflow input we report it as unsupported,
959 /// unless the input comes from a global definition that we are able to encode.
960 ///
961 /// # Returns
962 ///
963 /// - [`EncodeStatus::Success`] if all node inputs are supported.
964 /// - [`EncodeStatus::Unsupported`] if the node has unsupported non-local dataflow inputs, and we should mark it as unsupported.
965 fn encode_nonlocal_inputs(
966 &mut self,
967 node: H::Node,
968 optype: &OpType,
969 hugr: &H,
970 ) -> Result<EncodeStatus, PytketEncodeError<H::Node>> {
971 let node_parent = hugr.get_parent(node);
972
973 // Explore the dataflow value and static inputs, but not the _other inputs_.
974 let input_ports = hugr
975 .node_inputs(node)
976 .take(optype.value_input_count() + optype.static_input_port().is_some() as usize);
977
978 for (neigh, neigh_port) in input_ports.flat_map(|inp| hugr.linked_outputs(node, inp)) {
979 let wire = Wire::new(neigh, neigh_port);
980 if self.values.peek_wire_values(wire).is_some() {
981 // Ignore inputs that already have registered values.
982 continue;
983 }
984
985 let neigh_parent = hugr.get_parent(neigh);
986 if neigh_parent == node_parent {
987 continue;
988 }
989 if neigh_parent != Some(hugr.module_root()) {
990 // Non-global dataflow input, report as unsupported.
991 return Ok(EncodeStatus::Unsupported);
992 }
993 let optype = hugr.get_optype(neigh);
994 match optype {
995 OpType::FuncDefn(_) | OpType::FuncDecl(_) => {
996 // Function definitions/declarations have special handling to be able to encode Call nodes.
997 // We register them here with an empty set of values (since function-typed wires do not carry pytket values).
998 self.values
999 .register_wire::<TrackedValue>(wire, vec![], hugr)?;
1000 }
1001 OpType::Const(_) => {
1002 if self.try_encode_node(neigh, hugr)? == EncodeStatus::Unsupported {
1003 return Ok(EncodeStatus::Unsupported);
1004 }
1005 }
1006 _ => {
1007 return Ok(EncodeStatus::Unsupported);
1008 }
1009 }
1010 }
1011 Ok(EncodeStatus::Success)
1012 }
1013
1014 /// Check if a node has order edges to nodes outside the region.
1015 ///
1016 /// If that's the case, we don't try to encode the node and report it as
1017 /// unsupported instead.
1018 fn has_order_edges(&mut self, node: H::Node, optype: &OpType, hugr: &H) -> bool {
1019 optype
1020 .other_port(Direction::Incoming)
1021 .iter()
1022 .chain(optype.other_port(Direction::Outgoing).iter())
1023 .any(|&p| hugr.is_linked(node, p))
1024 }
1025
1026 /// Helper to register values for a node's output wires.
1027 ///
1028 /// Returns any new value associated with the output wires.
1029 ///
1030 /// ## Arguments
1031 ///
1032 /// - `node`: The node to register the outputs for.
1033 /// - `circ`: The circuit containing the node.
1034 /// - `input_qubits`: The qubit inputs to the operation.
1035 /// - `input_bits`: The bit inputs to the operation.
1036 /// - `input_params`: The list of input parameter expressions.
1037 /// - `options`: Options for controlling the output qubit, bits, and
1038 /// parameter expressions.
1039 /// - `wire_filter`: A function that takes a wire and returns true if the wire
1040 /// at the output of the `node` should be registered.
1041 #[expect(clippy::too_many_arguments)]
1042 fn register_node_outputs(
1043 &mut self,
1044 node: H::Node,
1045 hugr: &H,
1046 input_qubits: &[TrackedQubit],
1047 input_bits: &[TrackedBit],
1048 input_params: &[String],
1049 options: EmitCommandOptions,
1050 wire_filter: impl Fn(Wire<H::Node>) -> bool,
1051 ) -> Result<TrackedValues, PytketEncodeError<H::Node>> {
1052 let output_counts = self.node_output_values(node, hugr)?;
1053 let total_out_count: RegisterCount = output_counts.iter().map(|(_, c)| *c).sum();
1054
1055 let output_qubits = match options.reuse_qubits_fn {
1056 Some(f) => f(input_qubits),
1057 None => input_qubits.to_vec(),
1058 };
1059 let output_bits = match options.reuse_bits_fn {
1060 Some(f) => f(input_bits),
1061 None => input_bits.to_vec(),
1062 };
1063
1064 // Flag the input qubits that don't appear in the output as unused.
1065 let used_qubits: HashSet<TrackedQubit> = output_qubits.iter().copied().collect();
1066 for qb in input_qubits {
1067 if !used_qubits.contains(qb) {
1068 self.values.free_qubit(*qb);
1069 }
1070 }
1071
1072 // Compute all the output parameters at once
1073 let out_params = match options.output_params_fn {
1074 Some(f) => f(OutputParamArgs {
1075 expected_count: total_out_count.params,
1076 input_params,
1077 }),
1078 None => Vec::new(),
1079 };
1080
1081 // Check that we got the expected number of outputs.
1082 if out_params.len() != total_out_count.params {
1083 return Err(PytketEncodeError::custom(format!(
1084 "Expected {} parameters in the input values for a {}, but got {}.",
1085 total_out_count.params,
1086 hugr.get_optype(node),
1087 out_params.len()
1088 )));
1089 }
1090
1091 // Update the values in the node's outputs.
1092 //
1093 // We preserve the order of linear values in the input
1094 let mut qubits = output_qubits.iter().copied();
1095 let mut bits = output_bits.iter().copied();
1096 let mut params = out_params.into_iter();
1097 let mut new_outputs = TrackedValues::default();
1098 for (wire, count) in output_counts {
1099 if !wire_filter(wire) {
1100 continue;
1101 }
1102
1103 let mut out_wire_values = Vec::with_capacity(count.total());
1104
1105 // Qubits
1106 out_wire_values.extend(qubits.by_ref().take(count.qubits).map(TrackedValue::Qubit));
1107 for _ in out_wire_values.len()..count.qubits {
1108 // If we already assigned all input qubit ids, get a fresh one.
1109 let qb = self.values.new_qubit();
1110 new_outputs.qubits.push(qb);
1111 out_wire_values.push(TrackedValue::Qubit(qb));
1112 }
1113
1114 // Bits
1115 let non_bit_count = out_wire_values.len();
1116 out_wire_values.extend(bits.by_ref().take(count.bits).map(TrackedValue::Bit));
1117 let reused_bit_count = out_wire_values.len() - non_bit_count;
1118 for _ in reused_bit_count..count.bits {
1119 let b = self.values.new_bit();
1120 new_outputs.bits.push(b);
1121 out_wire_values.push(TrackedValue::Bit(b));
1122 }
1123
1124 // Parameters
1125 for expr in params.by_ref().take(count.params) {
1126 let p = self.values.new_param(expr);
1127 new_outputs.params.push(p);
1128 out_wire_values.push(p.into());
1129 }
1130 self.values.register_wire(wire, out_wire_values, hugr)?;
1131 }
1132
1133 Ok(new_outputs)
1134 }
1135
1136 /// Helper to register values for a singular output wire.
1137 ///
1138 /// In general, you should prefer
1139 /// [`PytketEncoderContext::register_node_outputs`] to register values for a
1140 /// node's multiple output wires at once.
1141 ///
1142 /// Returns any new value associated with the output wire.
1143 ///
1144 /// ## Arguments
1145 ///
1146 /// - `node`: The node to register the outputs for.
1147 /// - `circ`: The circuit containing the node.
1148 /// - `qubits`: The qubit registers to use for the output. Values are
1149 /// consumed from this slice as needed, and dropped from the slice as they
1150 /// are used.
1151 /// - `bits`: The bit registers to use for the output. Values are consumed
1152 /// from this slice as needed, and dropped from the slice as they are
1153 /// used.
1154 /// - `input_params`: The list of input parameter expressions.
1155 /// - `options_params_fn`: A function that computes the output parameter
1156 /// expressions given the inputs.
1157 #[expect(clippy::too_many_arguments)]
1158 fn register_port_output(
1159 &mut self,
1160 node: H::Node,
1161 port: OutgoingPort,
1162 hugr: &H,
1163 qubits: &mut &[TrackedQubit],
1164 bits: &mut &[TrackedBit],
1165 input_params: &[String],
1166 output_params_fn: impl FnOnce(OutputParamArgs<'_>) -> Vec<String>,
1167 ) -> Result<TrackedValues, PytketEncodeError<H::Node>> {
1168 let wire = Wire::new(node, port);
1169
1170 let Some(ty) = hugr
1171 .signature(node)
1172 .and_then(|s| s.out_port_type(port).cloned())
1173 else {
1174 return Ok(TrackedValues::default());
1175 };
1176
1177 let Some(count) = self.config().type_to_pytket(&ty) else {
1178 return Err(PytketEncodeError::custom(format!(
1179 "Found an unsupported type {ty} while encoding {port} of {node}."
1180 )));
1181 };
1182
1183 // Compute all the output parameters at once
1184 let out_params = output_params_fn(OutputParamArgs {
1185 expected_count: count.params,
1186 input_params,
1187 });
1188
1189 // Check that we got the expected number of outputs.
1190 if out_params.len() != count.params {
1191 return Err(PytketEncodeError::custom(format!(
1192 "Expected {} parameters in the input values for a {} at {port} of {node}, but got {}.",
1193 count.params,
1194 hugr.get_optype(node),
1195 out_params.len()
1196 )));
1197 }
1198
1199 // Update the values in the node's outputs.
1200 //
1201 // We preserve the order of linear values in the input
1202 let mut new_outputs = TrackedValues::default();
1203 let mut out_wire_values = Vec::with_capacity(count.total());
1204
1205 // Qubits
1206 // Reuse the ones from `qubits`, dropping them from the slice,
1207 // and allocate new ones as needed.
1208 let output_qubits = match qubits.split_off(..count.qubits) {
1209 Some(reused_qubits) => reused_qubits.to_vec(),
1210 None => {
1211 // Not enough qubits, allocate some fresh ones.
1212 let mut head_qubits = qubits.to_vec();
1213 *qubits = &[];
1214 let new_qubits = (head_qubits.len()..count.qubits).map(|_| {
1215 let q = self.values.new_qubit();
1216 new_outputs.qubits.push(q);
1217 q
1218 });
1219 head_qubits.extend(new_qubits);
1220 head_qubits
1221 }
1222 };
1223 out_wire_values.extend(output_qubits.iter().map(|&q| TrackedValue::Qubit(q)));
1224
1225 // Bits
1226 // Reuse the ones from `bits`, dropping them from the slice,
1227 // and allocate new ones as needed.
1228 let output_bits = match bits.split_off(..count.bits) {
1229 Some(reused_bits) => reused_bits.to_vec(),
1230 None => {
1231 // Not enough bits, allocate some fresh ones.
1232 let mut head_bits = bits.to_vec();
1233 *bits = &[];
1234 let new_bits = (head_bits.len()..count.bits).map(|_| {
1235 let b = self.values.new_bit();
1236 new_outputs.bits.push(b);
1237 b
1238 });
1239 head_bits.extend(new_bits);
1240 head_bits
1241 }
1242 };
1243 out_wire_values.extend(output_bits.iter().map(|&b| TrackedValue::Bit(b)));
1244
1245 // Parameters
1246 for expr in out_params.into_iter().take(count.params) {
1247 let p = self.values.new_param(expr);
1248 new_outputs.params.push(p);
1249 out_wire_values.push(p.into());
1250 }
1251 self.values.register_wire(wire, out_wire_values, hugr)?;
1252
1253 Ok(new_outputs)
1254 }
1255
1256 /// Return the output wires of a node that have an associated pytket [`RegisterCount`].
1257 #[expect(clippy::type_complexity)]
1258 fn node_output_values(
1259 &self,
1260 node: H::Node,
1261 hugr: &H,
1262 ) -> Result<Vec<(Wire<H::Node>, RegisterCount)>, PytketEncodeError<H::Node>> {
1263 let op = hugr.get_optype(node);
1264 let signature = op.dataflow_signature();
1265 let static_output = op.static_output_port();
1266 let other_output = op.other_output_port();
1267 let mut wire_counts = Vec::with_capacity(hugr.num_outputs(node));
1268 for out_port in hugr.node_outputs(node) {
1269 let ty = if Some(out_port) == other_output {
1270 // Ignore order edges
1271 continue;
1272 } else if Some(out_port) == static_output {
1273 let EdgeKind::Const(ty) = op.static_output().unwrap() else {
1274 return Err(PytketEncodeError::custom(format!(
1275 "Cannot emit a static output for a {op}."
1276 )));
1277 };
1278 ty
1279 } else {
1280 let Some(ty) = signature
1281 .as_ref()
1282 .and_then(|s| s.out_port_type(out_port).cloned())
1283 else {
1284 return Err(PytketEncodeError::custom(
1285 "Cannot emit a transparent node without a dataflow signature.",
1286 ));
1287 };
1288 ty
1289 };
1290
1291 let wire = hugr::Wire::new(node, out_port);
1292 let Some(count) = self.config().type_to_pytket(&ty) else {
1293 return Err(PytketEncodeError::custom(format!(
1294 "Found an unsupported type {ty} while encoding a {op}."
1295 )));
1296 };
1297 wire_counts.push((wire, count));
1298 }
1299 Ok(wire_counts)
1300 }
1301}
1302
1303/// Result of trying to encode a node in the Hugr.
1304///
1305/// This flag indicates that either
1306/// - The operation was successful encoded and is now registered on the
1307/// [`PytketEncoderContext`]
1308/// - The node cannot be encoded, and the context was left unchanged.
1309///
1310/// The latter is a recoverable error, as it promises that the context wasn't
1311/// modified. For non-recoverable errors, see [`PytketEncodeError`].
1312#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, derive_more::Display)]
1313pub enum EncodeStatus {
1314 /// The node was successfully encoded and registered in the context.
1315 Success,
1316 /// The node could not be encoded, and the context was left unchanged.
1317 Unsupported,
1318}
1319
1320/// Input arguments to the output parameter computation methods in the the emit_*
1321/// functions of [`PytketEncoderContext`].
1322#[derive(Clone, Copy, Debug)]
1323pub struct OutputParamArgs<'a> {
1324 /// The expected number of output parameters.
1325 pub expected_count: usize,
1326 /// The list of input parameter expressions.
1327 pub input_params: &'a [String],
1328}
1329
1330/// Input arguments to the output parameter computation method in
1331/// [`PytketEncoderContext::emit_node_command`].
1332///
1333/// This can be passed to [`make_tk1_operation`] to create a pytket
1334/// [`circuit_json::Operation`].
1335#[derive(Clone, Debug)]
1336pub struct MakeOperationArgs<'a> {
1337 /// Number of input qubits.
1338 pub num_qubits: usize,
1339 /// Number of input bits.
1340 pub num_bits: usize,
1341 /// List of input parameter expressions.
1342 pub params: Cow<'a, [String]>,
1343}
1344
1345/// Tracked values in a node's inputs, and any remaining input wire with missing
1346/// value information.
1347///
1348/// In most cases, finding an unsupported wire should be an error (see
1349/// [`NodeInputValues::try_into_tracked_values`]).
1350///
1351/// Auxiliary struct returned by
1352/// [`PytketEncoderContext::get_input_values_internal`]
1353struct NodeInputValues<N> {
1354 /// Tracked values originating in the local region.
1355 pub tracked_values: TrackedValues,
1356 /// Untracked inputs, with unknown values.
1357 pub unknown_values: Vec<Wire<N>>,
1358}
1359
1360impl<N: HugrNode> NodeInputValues<N> {
1361 /// Return the tracked values in the node's inputs.
1362 ///
1363 /// # Errors
1364 /// - [`OpConvertError::WireHasNoValues`] if there were any unknown wires.
1365 pub fn try_into_tracked_values(self) -> Result<TrackedValues, PytketEncodeError<N>> {
1366 match self.unknown_values.is_empty() {
1367 true => Ok(self.tracked_values),
1368 false => Err(PytketEncodeOpError::WireHasNoValues {
1369 wire: self.unknown_values[0],
1370 }
1371 .into()),
1372 }
1373 }
1374}
1375
1376/// Cached value for a function encoding.
1377///
1378/// If the function contains output parameters, it is unsupported
1379/// and should be emitted as an unsupported op instead.
1380#[derive(Clone, Debug)]
1381enum CachedEncodedFunction {
1382 /// Successfully encoded function.
1383 Encoded {
1384 /// The serialised circuit for the function.
1385 serial_circuit: SerialCircuit,
1386 },
1387 /// Unsupported function
1388 Unsupported,
1389 /// A marker for functions currently being encoded.
1390 ///
1391 /// Used to detect recursive calls, and prevent infinite recursion.
1392 InEncodingStack,
1393}
1394
1395/// Initialize a tket1 [Operation](circuit_json::Operation) to pass to
1396/// [`PytketEncoderContext::emit_command`].
1397///
1398/// ## Arguments
1399/// - `pytket_optype`: The operation type to use.
1400/// - `qubit_count`: The number of qubits used by the operation.
1401/// - `bit_count`: The number of linear bits used by the operation.
1402/// - `params`: Parameters of the operation, expressed as string expressions.
1403/// Normally obtained from [`ValueTracker::param_expression`].
1404pub fn make_tk1_operation(
1405 pytket_optype: tket_json_rs::OpType,
1406 inputs: MakeOperationArgs<'_>,
1407) -> circuit_json::Operation {
1408 let mut op = circuit_json::Operation::default();
1409 op.op_type = pytket_optype;
1410 op.n_qb = Some(inputs.num_qubits as u32);
1411 op.params = match inputs.params.is_empty() {
1412 false => Some(inputs.params.into_owned()),
1413 true => None,
1414 };
1415 op.signature = Some(
1416 [
1417 vec!["Q".into(); inputs.num_qubits],
1418 vec!["B".into(); inputs.num_bits],
1419 ]
1420 .concat(),
1421 );
1422 op
1423}
1424
1425/// Initialize a tket1 [Operation](circuit_json::Operation) that only operates
1426/// on classical values.
1427///
1428/// This method is specific to some classical operations in
1429/// [`tket_json_rs::OpType`]. For the classical _expressions_ in
1430/// [`tket_json_rs::OpType::ClExpr`] / [`tket_json_rs::clexpr::op::ClOp`] use
1431/// [`make_tk1_classical_expression`].
1432///
1433/// This can be passed to [`PytketEncoderContext::emit_command`].
1434///
1435/// See [`make_tk1_operation`] for a more general case.
1436///
1437/// ## Arguments
1438/// - `pytket_optype`: The operation type to use.
1439/// - `bit_count`: The number of linear bits used by the operation.
1440/// - `classical`: The parameters to the classical operation.
1441pub fn make_tk1_classical_operation(
1442 pytket_optype: tket_json_rs::OpType,
1443 bit_count: usize,
1444 classical: tket_json_rs::circuit_json::Classical,
1445) -> tket_json_rs::circuit_json::Operation {
1446 let args = MakeOperationArgs {
1447 num_qubits: 0,
1448 num_bits: bit_count,
1449 params: Cow::Borrowed(&[]),
1450 };
1451 let mut op = make_tk1_operation(pytket_optype, args);
1452 op.classical = Some(Box::new(classical));
1453 op
1454}
1455
1456/// Initialize a tket1 [Operation](circuit_json::Operation) that represents a
1457/// classical expression.
1458///
1459/// This method is specific to [`tket_json_rs::OpType::ClExpr`] /
1460/// [`tket_json_rs::clexpr::op::ClOp`]. For other classical operations in
1461/// [`tket_json_rs::OpType`] use [`make_tk1_classical_operation`].
1462///
1463/// Classical expressions are a bit different from other operations in pytket as
1464/// they refer to their inputs and outputs by their position in the operation's
1465/// bit and register list. See the arguments below for more details.
1466///
1467/// This can be passed to [`PytketEncoderContext::emit_command`]. See
1468/// [`make_tk1_operation`] for a more general case.
1469///
1470/// ## Arguments
1471/// - `bit_count`: The number of bits (both inputs and outputs) referenced by
1472/// the operation. The operation will refer to the bits by an index in the
1473/// range `0..bit_count`.
1474/// - `output_bits`: Slice of bit indices in `0..bit_count` that are the outputs
1475/// of the operation.
1476/// - `registers`: groups of bits that are used as registers in the operation.
1477/// Each group is a slice of bit indices in `0..bit_count`, plus a register
1478/// identifier. The operation may refer to these registers.
1479/// - `expression`: The classical expression, expressed in term of the local
1480/// bit and register indices.
1481pub fn make_tk1_classical_expression(
1482 bit_count: usize,
1483 output_bits: &[u32],
1484 registers: &[InputClRegister],
1485 expression: ClOperator,
1486) -> tket_json_rs::circuit_json::Operation {
1487 let mut bit_vars = Vec::new();
1488 collect_clexpr_bit_vars(&expression, &mut bit_vars);
1489 bit_vars.sort_unstable();
1490 bit_vars.dedup();
1491
1492 let mut clexpr = tket_json_rs::clexpr::ClExpr::default();
1493 clexpr.bit_posn = bit_vars.into_iter().map(|i| (i, i)).collect();
1494 clexpr.reg_posn = registers.to_vec();
1495 clexpr.output_posn = tket_json_rs::clexpr::ClRegisterBits(output_bits.to_vec());
1496 clexpr.expr = expression;
1497
1498 let args = MakeOperationArgs {
1499 num_qubits: 0,
1500 num_bits: bit_count,
1501 params: Cow::Borrowed(&[]),
1502 };
1503 let mut op = make_tk1_operation(tket_json_rs::OpType::ClExpr, args);
1504 op.classical_expr = Some(clexpr);
1505 op
1506}
1507
1508/// Collect the local bit variables referenced by a classical expression.
1509///
1510/// `ClExpr::bit_posn` describes variables used by the expression tree. Fresh
1511/// output-only bits are listed in `output_posn`, but must not be declared as
1512/// expression variables.
1513fn collect_clexpr_bit_vars(expression: &ClOperator, bit_vars: &mut Vec<u32>) {
1514 for arg in &expression.args {
1515 match arg {
1516 ClArgument::Terminal(ClTerminal::Variable(ClVariable::Bit { index })) => {
1517 bit_vars.push(*index);
1518 }
1519 ClArgument::Expression(expression) => {
1520 collect_clexpr_bit_vars(expression, bit_vars);
1521 }
1522 _ => {}
1523 }
1524 }
1525}