Skip to main content

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, &params, 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(&params),
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}