hugr_core/hugr/views/
sibling_subgraph.rs

1//! Views for HUGR sibling subgraphs.
2//!
3//! Views into convex subgraphs of HUGRs within a single level of the
4//! hierarchy, i.e. within a sibling graph. Convex subgraph are always
5//! induced subgraphs, i.e. they are defined by a subset of the sibling nodes.
6//!
7//! Sibling subgraphs complement [`super::HierarchyView`]s in the sense that the
8//! latter provide views for subgraphs defined by hierarchical relationships,
9//! while the former provide views for subgraphs within a single level of the
10//! hierarchy.
11
12use std::cell::OnceCell;
13use std::collections::HashSet;
14use std::mem;
15
16use itertools::Itertools;
17use portgraph::algorithms::ConvexChecker;
18use portgraph::boundary::Boundary;
19use portgraph::{view::Subgraph, Direction, PortView};
20use thiserror::Error;
21
22use crate::builder::{Container, FunctionBuilder};
23use crate::extension::ExtensionSet;
24use crate::hugr::{HugrMut, HugrView, RootTagged};
25use crate::ops::dataflow::DataflowOpTrait;
26use crate::ops::handle::{ContainerHandle, DataflowOpID};
27use crate::ops::{NamedOp, OpTag, OpTrait, OpType};
28use crate::types::{Signature, Type};
29use crate::{Hugr, IncomingPort, Node, OutgoingPort, Port, SimpleReplacement};
30
31/// A non-empty convex subgraph of a HUGR sibling graph.
32///
33/// A HUGR region in which all nodes share the same parent. Unlike
34/// [`super::SiblingGraph`],  not all nodes of the sibling graph must be
35/// included. A convex subgraph is always an induced subgraph, i.e. it is defined
36/// by a set of nodes and all edges between them.
37///
38/// The incoming boundary (resp. outgoing boundary) is given by the input (resp.
39/// output) ports of the subgraph that are linked to nodes outside of the subgraph.
40/// The signature of the subgraph is then given by the types of the incoming
41/// and outgoing boundary ports. Given a replacement with the same signature,
42/// a [`SimpleReplacement`] can be constructed to rewrite the subgraph with the
43/// replacement.
44///
45/// The ordering of the nodes in the subgraph is irrelevant to define the convex
46/// subgraph, but it determines the ordering of the boundary signature.
47///
48/// No reference to the underlying graph is kept. Thus most of the subgraph
49/// methods expect a reference to the Hugr as an argument.
50///
51/// At the moment we do not support state order edges at the subgraph boundary.
52/// The `boundary_port` and `signature` methods will panic if any are found.
53/// State order edges are also unsupported in replacements in
54/// `create_simple_replacement`.
55// TODO: implement a borrowing wrapper that implements a view into the Hugr
56// given a reference.
57#[derive(Clone, Debug)]
58pub struct SiblingSubgraph {
59    /// The nodes of the induced subgraph.
60    nodes: Vec<Node>,
61    /// The input ports of the subgraph.
62    ///
63    /// Grouped by input parameter. Each port must be unique and belong to a
64    /// node in `nodes`.
65    inputs: Vec<Vec<(Node, IncomingPort)>>,
66    /// The output ports of the subgraph.
67    ///
68    /// Repeated ports are allowed and correspond to copying the output. Every
69    /// port must belong to a node in `nodes`.
70    outputs: Vec<(Node, OutgoingPort)>,
71}
72
73/// The type of the incoming boundary of [`SiblingSubgraph`].
74///
75/// The nested vec represents a partition of the incoming boundary ports by
76/// input parameter. A set in the partition that has more than one element
77/// corresponds to an input parameter that is copied and useful multiple times
78/// in the subgraph.
79pub type IncomingPorts = Vec<Vec<(Node, IncomingPort)>>;
80/// The type of the outgoing boundary of [`SiblingSubgraph`].
81pub type OutgoingPorts = Vec<(Node, OutgoingPort)>;
82
83impl SiblingSubgraph {
84    /// A sibling subgraph from a [`crate::ops::OpTag::DataflowParent`]-rooted
85    /// HUGR.
86    ///
87    /// The subgraph is given by the nodes between the input and output children
88    /// nodes of the root node. If you wish to create a subgraph from another
89    /// root, wrap the `region` argument in a [`super::SiblingGraph`].
90    ///
91    /// Wires connecting the input and output nodes are ignored. Note that due
92    /// to this the resulting subgraph's signature may not match the signature
93    /// of the dataflow parent.
94    ///
95    /// This will return an [`InvalidSubgraph::EmptySubgraph`] error if the
96    /// subgraph is empty.
97    pub fn try_new_dataflow_subgraph<H, Root>(dfg_graph: &H) -> Result<Self, InvalidSubgraph>
98    where
99        H: Clone + RootTagged<RootHandle = Root>,
100        Root: ContainerHandle<ChildrenHandle = DataflowOpID>,
101    {
102        let parent = dfg_graph.root();
103        let nodes = dfg_graph.children(parent).skip(2).collect_vec();
104        let (inputs, outputs) = get_input_output_ports(dfg_graph);
105
106        validate_subgraph(dfg_graph, &nodes, &inputs, &outputs)?;
107
108        if nodes.is_empty() {
109            Err(InvalidSubgraph::EmptySubgraph)
110        } else {
111            Ok(Self {
112                nodes,
113                inputs,
114                outputs,
115            })
116        }
117    }
118
119    /// Create a new convex sibling subgraph from input and output boundaries.
120    ///
121    /// Any sibling subgraph can be defined using two sets of boundary edges
122    /// $B_I$ and $B_O$, the incoming and outgoing boundary edges respectively.
123    /// Intuitively, the sibling subgraph is all the edges and nodes "between"
124    /// an edge of $B_I$ and an edge of $B_O$.
125    ///
126    /// ## Definition
127    ///
128    /// More formally, the sibling subgraph of a graph $G = (V, E)$ given
129    /// by sets of incoming and outgoing boundary edges $B_I, B_O \subseteq E$
130    /// is the graph given by the connected components of the graph
131    /// $G' = (V, E \ B_I \ B_O)$ that contain at least one node that is either
132    ///  - the target of an incoming boundary edge, or
133    ///  - the source of an outgoing boundary edge.
134    ///
135    /// A subgraph is well-formed if for every edge in the HUGR
136    ///  - it is in $B_I$ if and only if it has a source outside of the subgraph
137    ///    and a target inside of it, and
138    ///  - it is in $B_O$ if and only if it has a source inside of the subgraph
139    ///    and a target outside of it.
140    ///
141    /// ## Arguments
142    ///
143    /// The `incoming` and `outgoing` arguments give $B_I$ and $B_O$ respectively.
144    /// Incoming edges must be given by incoming ports and outgoing edges by
145    /// outgoing ports. The ordering of the incoming and outgoing ports defines
146    /// the signature of the subgraph.
147    ///
148    /// Incoming boundary ports must be unique and partitioned by input
149    /// parameter: two ports within the same set of the partition must be
150    /// copyable and will result in the input being copied. Outgoing
151    /// boundary ports are given in a list and can appear multiple times if
152    /// they are copyable, in which case the output will be copied.
153    ///
154    /// ## Errors
155    ///
156    /// This function fails if the subgraph is not convex, if the nodes
157    /// do not share a common parent or if the subgraph is empty.
158    pub fn try_new(
159        incoming: IncomingPorts,
160        outgoing: OutgoingPorts,
161        hugr: &impl HugrView,
162    ) -> Result<Self, InvalidSubgraph> {
163        let checker = TopoConvexChecker::new(hugr);
164        Self::try_new_with_checker(incoming, outgoing, hugr, &checker)
165    }
166
167    /// Create a new convex sibling subgraph from input and output boundaries.
168    ///
169    /// Provide a [`ConvexChecker`] instance to avoid constructing one for
170    /// faster convexity check. If you do not have one, use
171    /// [`SiblingSubgraph::try_new`].
172    ///
173    /// Refer to [`SiblingSubgraph::try_new`] for the full
174    /// documentation.
175    pub fn try_new_with_checker(
176        inputs: IncomingPorts,
177        outputs: OutgoingPorts,
178        hugr: &impl HugrView,
179        checker: &impl ConvexChecker,
180    ) -> Result<Self, InvalidSubgraph> {
181        let pg = hugr.portgraph();
182
183        // Ordering of the edges here is preserved and becomes ordering of the signature.
184        let subpg = Subgraph::new_subgraph(pg.clone(), make_boundary(hugr, &inputs, &outputs));
185        let nodes = subpg.nodes_iter().map_into().collect_vec();
186        validate_subgraph(hugr, &nodes, &inputs, &outputs)?;
187
188        if !subpg.is_convex_with_checker(checker) {
189            return Err(InvalidSubgraph::NotConvex);
190        }
191
192        Ok(Self {
193            nodes,
194            inputs,
195            outputs,
196        })
197    }
198
199    /// Create a subgraph from a set of nodes.
200    ///
201    /// The incoming boundary is given by the set of edges with a source
202    /// not in nodes and a target in nodes. Conversely, the outgoing boundary
203    /// is given by the set of edges with a source in nodes and a target not
204    /// in nodes.
205    ///
206    /// The subgraph signature will be given by the types of the incoming and
207    /// outgoing edges ordered by the node order in `nodes` and within each node
208    /// by the port order.
209    ///
210    /// The in- and out-arity of the signature will match the
211    /// number of incoming and outgoing edges respectively. In particular, the
212    /// assumption is made that no two incoming edges have the same source
213    /// (no copy nodes at the input boundary).
214    pub fn try_from_nodes(
215        nodes: impl Into<Vec<Node>>,
216        hugr: &impl HugrView,
217    ) -> Result<Self, InvalidSubgraph> {
218        let checker = TopoConvexChecker::new(hugr);
219        Self::try_from_nodes_with_checker(nodes, hugr, &checker)
220    }
221
222    /// Create a subgraph from a set of nodes.
223    ///
224    /// Provide a [`ConvexChecker`] instance to avoid constructing one for
225    /// faster convexity check. If you do not have one, use
226    /// [`SiblingSubgraph::try_from_nodes`].
227    ///
228    /// Refer to [`SiblingSubgraph::try_from_nodes`] for the full
229    /// documentation.
230    pub fn try_from_nodes_with_checker<'c, 'h: 'c, H: HugrView>(
231        nodes: impl Into<Vec<Node>>,
232        hugr: &'h H,
233        checker: &impl ConvexChecker,
234    ) -> Result<Self, InvalidSubgraph> {
235        let nodes = nodes.into();
236
237        // If there's one or less nodes, we don't need to check convexity.
238        match nodes.as_slice() {
239            [] => return Err(InvalidSubgraph::EmptySubgraph),
240            [node] => return Ok(Self::from_node(*node, hugr)),
241            _ => {}
242        };
243
244        let nodes_set = nodes.iter().copied().collect::<HashSet<_>>();
245        let incoming_edges = nodes
246            .iter()
247            .flat_map(|&n| hugr.node_inputs(n).map(move |p| (n, p)));
248        let outgoing_edges = nodes
249            .iter()
250            .flat_map(|&n| hugr.node_outputs(n).map(move |p| (n, p)));
251        let inputs = incoming_edges
252            .filter(|&(n, p)| {
253                if !hugr.is_linked(n, p) {
254                    return false;
255                }
256                let (out_n, _) = hugr.single_linked_output(n, p).unwrap();
257                !nodes_set.contains(&out_n)
258            })
259            // Every incoming edge is its own input.
260            .map(|p| vec![p])
261            .collect_vec();
262        let outputs = outgoing_edges
263            .filter(|&(n, p)| {
264                hugr.linked_ports(n, p)
265                    .any(|(n1, _)| !nodes_set.contains(&n1))
266            })
267            .collect_vec();
268        Self::try_new_with_checker(inputs, outputs, hugr, checker)
269    }
270
271    /// Create a subgraph containing a single node.
272    ///
273    /// The subgraph signature will be given by signature of the node.
274    pub fn from_node(node: Node, hugr: &impl HugrView) -> Self {
275        let nodes = vec![node];
276        let inputs = hugr
277            .node_inputs(node)
278            .filter(|&p| hugr.is_linked(node, p))
279            .map(|p| vec![(node, p)])
280            .collect_vec();
281        let outputs = hugr
282            .node_outputs(node)
283            .filter_map(|p| {
284                // accept linked outputs or unlinked value outputs
285                {
286                    hugr.is_linked(node, p)
287                        || hugr
288                            .get_optype(node)
289                            .port_kind(p)
290                            .is_some_and(|k| k.is_value())
291                }
292                .then_some((node, p))
293            })
294            .collect_vec();
295
296        Self {
297            nodes,
298            inputs,
299            outputs,
300        }
301    }
302
303    /// An iterator over the nodes in the subgraph.
304    pub fn nodes(&self) -> &[Node] {
305        &self.nodes
306    }
307
308    /// The number of nodes in the subgraph.
309    pub fn node_count(&self) -> usize {
310        self.nodes.len()
311    }
312
313    /// Returns the computed [`IncomingPorts`] of the subgraph.
314    pub fn incoming_ports(&self) -> &IncomingPorts {
315        &self.inputs
316    }
317
318    /// Returns the computed [`OutgoingPorts`] of the subgraph.
319    pub fn outgoing_ports(&self) -> &OutgoingPorts {
320        &self.outputs
321    }
322
323    /// The signature of the subgraph.
324    pub fn signature(&self, hugr: &impl HugrView) -> Signature {
325        let input = self
326            .inputs
327            .iter()
328            .map(|part| {
329                let &(n, p) = part.iter().next().expect("is non-empty");
330                let sig = hugr.signature(n).expect("must have dataflow signature");
331                sig.port_type(p).cloned().expect("must be dataflow edge")
332            })
333            .collect_vec();
334        let output = self
335            .outputs
336            .iter()
337            .map(|&(n, p)| {
338                let sig = hugr.signature(n).expect("must have dataflow signature");
339                sig.port_type(p).cloned().expect("must be dataflow edge")
340            })
341            .collect_vec();
342        Signature::new(input, output).with_extension_delta(ExtensionSet::union_over(
343            self.nodes
344                .iter()
345                .map(|n| hugr.get_optype(*n).extension_delta()),
346        ))
347    }
348
349    /// The parent of the sibling subgraph.
350    pub fn get_parent(&self, hugr: &impl HugrView) -> Node {
351        hugr.get_parent(self.nodes[0]).expect("invalid subgraph")
352    }
353
354    /// Construct a [`SimpleReplacement`] to replace `self` with `replacement`.
355    ///
356    /// `replacement` must be a hugr with DFG root and its signature must
357    /// match the signature of the subgraph.
358    ///
359    /// May return one of the following five errors
360    ///  - [`InvalidReplacement::InvalidDataflowGraph`]: the replacement
361    ///    graph is not a [`crate::ops::OpTag::DataflowParent`]-rooted graph,
362    ///  - [`InvalidReplacement::InvalidSignature`]: the signature of the
363    ///    replacement DFG does not match the subgraph signature, or
364    ///  - [`InvalidReplacement::NonConvexSubgraph`]: the sibling subgraph is not
365    ///    convex.
366    ///
367    /// At the moment we do not support state order edges. If any are found in
368    /// the replacement graph, this will panic.
369    pub fn create_simple_replacement(
370        &self,
371        hugr: &impl HugrView,
372        replacement: Hugr,
373    ) -> Result<SimpleReplacement, InvalidReplacement> {
374        let rep_root = replacement.root();
375        let dfg_optype = replacement.get_optype(rep_root);
376        if !OpTag::Dfg.is_superset(dfg_optype.tag()) {
377            return Err(InvalidReplacement::InvalidDataflowGraph {
378                node: rep_root,
379                op: dfg_optype.clone(),
380            });
381        }
382        let [rep_input, rep_output] = replacement
383            .get_io(rep_root)
384            .expect("DFG root in the replacement does not have input and output nodes.");
385
386        let current_signature = self.signature(hugr);
387        let new_signature = dfg_optype.dataflow_signature();
388        if new_signature.as_ref().map(|s| &s.input) != Some(&current_signature.input)
389            || new_signature.as_ref().map(|s| &s.output) != Some(&current_signature.output)
390        {
391            return Err(InvalidReplacement::InvalidSignature {
392                expected: self.signature(hugr),
393                actual: dfg_optype.dataflow_signature().map(|s| s.into_owned()),
394            });
395        }
396
397        // TODO: handle state order edges. For now panic if any are present.
398        // See https://github.com/CQCL/hugr/discussions/432
399        let rep_inputs = replacement.node_outputs(rep_input).map(|p| (rep_input, p));
400        let rep_outputs = replacement.node_inputs(rep_output).map(|p| (rep_output, p));
401        let (rep_inputs, in_order_ports): (Vec<_>, Vec<_>) = rep_inputs.partition(|&(n, p)| {
402            replacement
403                .signature(n)
404                .is_some_and(|s| s.port_type(p).is_some())
405        });
406        let (rep_outputs, out_order_ports): (Vec<_>, Vec<_>) = rep_outputs.partition(|&(n, p)| {
407            replacement
408                .signature(n)
409                .is_some_and(|s| s.port_type(p).is_some())
410        });
411
412        if iter_io(&vec![out_order_ports], &in_order_ports)
413            .any(|(n, p)| is_order_edge(&replacement, n, p))
414        {
415            unimplemented!("Found state order edges in replacement graph");
416        }
417
418        let nu_inp = rep_inputs
419            .into_iter()
420            .zip_eq(&self.inputs)
421            .flat_map(|((rep_source_n, rep_source_p), self_targets)| {
422                replacement
423                    .linked_inputs(rep_source_n, rep_source_p)
424                    .flat_map(move |rep_target| {
425                        self_targets
426                            .iter()
427                            .map(move |&self_target| (rep_target, self_target))
428                    })
429            })
430            .collect();
431        let nu_out = self
432            .outputs
433            .iter()
434            .zip_eq(rep_outputs)
435            .flat_map(|(&(self_source_n, self_source_p), (_, rep_target_p))| {
436                hugr.linked_inputs(self_source_n, self_source_p)
437                    .map(move |self_target| (self_target, rep_target_p))
438            })
439            .collect();
440
441        Ok(SimpleReplacement::new(
442            self.clone(),
443            replacement,
444            nu_inp,
445            nu_out,
446        ))
447    }
448
449    /// Create a new Hugr containing only the subgraph.
450    ///
451    /// The new Hugr will contain a [FuncDefn][crate::ops::FuncDefn] root
452    /// with the same signature as the subgraph and the specified `name`
453    pub fn extract_subgraph(&self, hugr: &impl HugrView, name: impl Into<String>) -> Hugr {
454        let mut builder = FunctionBuilder::new(name, self.signature(hugr)).unwrap();
455        // Take the unfinished Hugr from the builder, to avoid unnecessary
456        // validation checks that require connecting the inputs and outputs.
457        let mut extracted = mem::take(builder.hugr_mut());
458        let node_map = extracted.insert_subgraph(extracted.root(), hugr, self);
459
460        // Connect the inserted nodes in-between the input and output nodes.
461        let [inp, out] = extracted.get_io(extracted.root()).unwrap();
462        let inputs = extracted.node_outputs(inp).zip(self.inputs.iter());
463        let outputs = extracted.node_inputs(out).zip(self.outputs.iter());
464        let mut connections = Vec::with_capacity(inputs.size_hint().0 + outputs.size_hint().0);
465
466        for (inp_port, repl_ports) in inputs {
467            for (repl_node, repl_port) in repl_ports {
468                connections.push((inp, inp_port, node_map[repl_node], *repl_port));
469            }
470        }
471        for (out_port, (repl_node, repl_port)) in outputs {
472            connections.push((node_map[repl_node], *repl_port, out, out_port));
473        }
474
475        for (src, src_port, dst, dst_port) in connections {
476            extracted.connect(src, src_port, dst, dst_port);
477        }
478
479        extracted
480    }
481}
482
483/// Returns an iterator over the input ports.
484fn iter_incoming(inputs: &IncomingPorts) -> impl Iterator<Item = (Node, IncomingPort)> + '_ {
485    inputs.iter().flat_map(|part| part.iter().copied())
486}
487
488/// Returns an iterator over the output ports.
489fn iter_outgoing(outputs: &OutgoingPorts) -> impl Iterator<Item = (Node, OutgoingPort)> + '_ {
490    outputs.iter().copied()
491}
492
493/// Returns an iterator over both incoming and outgoing ports.
494fn iter_io<'a>(
495    inputs: &'a IncomingPorts,
496    outputs: &'a OutgoingPorts,
497) -> impl Iterator<Item = (Node, Port)> + 'a {
498    iter_incoming(inputs)
499        .map(|(n, p)| (n, Port::from(p)))
500        .chain(iter_outgoing(outputs).map(|(n, p)| (n, Port::from(p))))
501}
502
503fn make_boundary<'a>(
504    hugr: &impl HugrView,
505    inputs: &'a IncomingPorts,
506    outputs: &'a OutgoingPorts,
507) -> Boundary {
508    let to_pg_index = |n: Node, p: Port| {
509        hugr.portgraph()
510            .port_index(n.pg_index(), p.pg_offset())
511            .unwrap()
512    };
513    Boundary::new(
514        iter_incoming(inputs).map(|(n, p)| to_pg_index(n, p.into())),
515        iter_outgoing(outputs).map(|(n, p)| to_pg_index(n, p.into())),
516    )
517}
518
519/// Precompute convexity information for a HUGR.
520///
521/// This can be used when constructing multiple sibling subgraphs to speed up
522/// convexity checking.
523pub struct TopoConvexChecker<'g, Base: 'g + HugrView> {
524    base: &'g Base,
525    checker: OnceCell<portgraph::algorithms::TopoConvexChecker<Base::Portgraph<'g>>>,
526}
527
528impl<'g, Base: HugrView> TopoConvexChecker<'g, Base> {
529    /// Create a new convexity checker.
530    pub fn new(base: &'g Base) -> Self {
531        Self {
532            base,
533            checker: OnceCell::new(),
534        }
535    }
536
537    /// Returns the portgraph convexity checker, initializing it if necessary.
538    fn get_checker(&self) -> &portgraph::algorithms::TopoConvexChecker<Base::Portgraph<'g>> {
539        self.checker
540            .get_or_init(|| portgraph::algorithms::TopoConvexChecker::new(self.base.portgraph()))
541    }
542}
543
544impl<Base: HugrView> ConvexChecker for TopoConvexChecker<'_, Base> {
545    fn is_convex(
546        &self,
547        nodes: impl IntoIterator<Item = portgraph::NodeIndex>,
548        inputs: impl IntoIterator<Item = portgraph::PortIndex>,
549        outputs: impl IntoIterator<Item = portgraph::PortIndex>,
550    ) -> bool {
551        let mut nodes = nodes.into_iter().multipeek();
552        // If the node iterator contains less than two nodes, the subgraph is
553        // trivially convex.
554        if nodes.peek().is_none() || nodes.peek().is_none() {
555            return true;
556        };
557        self.get_checker().is_convex(nodes, inputs, outputs)
558    }
559}
560
561/// The type of all ports in the iterator.
562///
563/// If the array is empty or a port does not exist, returns `None`.
564fn get_edge_type<H: HugrView, P: Into<Port> + Copy>(hugr: &H, ports: &[(Node, P)]) -> Option<Type> {
565    let &(n, p) = ports.first()?;
566    let edge_t = hugr.signature(n)?.port_type(p)?.clone();
567    ports
568        .iter()
569        .all(|&(n, p)| {
570            hugr.signature(n)
571                .is_some_and(|s| s.port_type(p) == Some(&edge_t))
572        })
573        .then_some(edge_t)
574}
575
576/// Whether a subgraph is valid.
577///
578/// Verifies that input and output ports are valid subgraph boundaries, i.e. they belong
579/// to nodes within the subgraph and are linked to at least one node outside of the subgraph.
580/// This does NOT check convexity proper, i.e. whether the set of nodes form a convex
581/// induced graph.
582fn validate_subgraph<H: HugrView>(
583    hugr: &H,
584    nodes: &[Node],
585    inputs: &IncomingPorts,
586    outputs: &OutgoingPorts,
587) -> Result<(), InvalidSubgraph> {
588    // Copy of the nodes for fast lookup.
589    let node_set = nodes.iter().copied().collect::<HashSet<_>>();
590
591    // Check nodes is not empty
592    if nodes.is_empty() {
593        return Err(InvalidSubgraph::EmptySubgraph);
594    }
595    // Check all nodes share parent
596    if !nodes.iter().map(|&n| hugr.get_parent(n)).all_equal() {
597        let first_node = nodes[0];
598        let first_parent = hugr.get_parent(first_node);
599        let other_node = *nodes
600            .iter()
601            .skip(1)
602            .find(|&&n| hugr.get_parent(n) != first_parent)
603            .unwrap();
604        let other_parent = hugr.get_parent(other_node);
605        return Err(InvalidSubgraph::NoSharedParent {
606            first_node,
607            first_parent,
608            other_node,
609            other_parent,
610        });
611    }
612
613    // Check there are no linked "other" ports
614    if iter_io(inputs, outputs).any(|(n, p)| is_order_edge(hugr, n, p)) {
615        unimplemented!("Connected order edges not supported at the boundary")
616    }
617
618    let boundary_ports = iter_io(inputs, outputs).collect_vec();
619    // Check that the boundary ports are all in the subgraph.
620    if let Some(&(n, p)) = boundary_ports.iter().find(|(n, _)| !node_set.contains(n)) {
621        Err(InvalidSubgraphBoundary::PortNodeNotInSet(n, p))?;
622    };
623    // Check that every inside port has at least one linked port outside.
624    if let Some(&(n, p)) = boundary_ports.iter().find(|&&(n, p)| {
625        hugr.linked_ports(n, p)
626            .all(|(n1, _)| node_set.contains(&n1))
627    }) {
628        Err(InvalidSubgraphBoundary::DisconnectedBoundaryPort(n, p))?;
629    };
630
631    // Check that every incoming port of a node in the subgraph whose source is not in the subgraph
632    // belongs to inputs.
633    if nodes.iter().any(|&n| {
634        hugr.node_inputs(n).any(|p| {
635            hugr.linked_ports(n, p).any(|(n1, _)| {
636                !node_set.contains(&n1) && !inputs.iter().any(|nps| nps.contains(&(n, p)))
637            })
638        })
639    }) {
640        return Err(InvalidSubgraph::NotConvex);
641    }
642    // Check that every outgoing port of a node in the subgraph whose target is not in the subgraph
643    // belongs to outputs.
644    if nodes.iter().any(|&n| {
645        hugr.node_outputs(n).any(|p| {
646            hugr.linked_ports(n, p)
647                .any(|(n1, _)| !node_set.contains(&n1) && !outputs.contains(&(n, p)))
648        })
649    }) {
650        return Err(InvalidSubgraph::NotConvex);
651    }
652
653    // Check inputs are unique
654    if !inputs.iter().flatten().all_unique() {
655        return Err(InvalidSubgraphBoundary::NonUniqueInput.into());
656    }
657
658    // Check no incoming partition is empty
659    if inputs.iter().any(|p| p.is_empty()) {
660        return Err(InvalidSubgraphBoundary::EmptyPartition.into());
661    }
662
663    // Check edge types are equal within partition and copyable if partition size > 1
664    if let Some((i, _)) = inputs.iter().enumerate().find(|(_, ports)| {
665        let Some(edge_t) = get_edge_type(hugr, ports) else {
666            return true;
667        };
668        let require_copy = ports.len() > 1;
669        require_copy && !edge_t.copyable()
670    }) {
671        Err(InvalidSubgraphBoundary::MismatchedTypes(i))?;
672    };
673
674    Ok(())
675}
676
677fn get_input_output_ports<H: HugrView>(hugr: &H) -> (IncomingPorts, OutgoingPorts) {
678    let [inp, out] = hugr.get_io(hugr.root()).expect("invalid DFG");
679    if has_other_edge(hugr, inp, Direction::Outgoing) {
680        unimplemented!("Non-dataflow output not supported at input node")
681    }
682    let dfg_inputs = hugr
683        .get_optype(inp)
684        .as_input()
685        .unwrap()
686        .signature()
687        .output_ports();
688    if has_other_edge(hugr, out, Direction::Incoming) {
689        unimplemented!("Non-dataflow input not supported at output node")
690    }
691    let dfg_outputs = hugr
692        .get_optype(out)
693        .as_output()
694        .unwrap()
695        .signature()
696        .input_ports();
697
698    // Collect for each port in the input the set of target ports, filtering
699    // direct wires to the output.
700    let inputs = dfg_inputs
701        .into_iter()
702        .map(|p| {
703            hugr.linked_inputs(inp, p)
704                .filter(|&(n, _)| n != out)
705                .collect_vec()
706        })
707        .filter(|v| !v.is_empty())
708        .collect();
709    // Collect for each port in the output the set of source ports, filtering
710    // direct wires to the input.
711    let outputs = dfg_outputs
712        .into_iter()
713        .filter_map(|p| hugr.linked_outputs(out, p).find(|&(n, _)| n != inp))
714        .collect();
715    (inputs, outputs)
716}
717
718/// Whether a port is linked to a state order edge.
719fn is_order_edge<H: HugrView>(hugr: &H, node: Node, port: Port) -> bool {
720    let op = hugr.get_optype(node);
721    op.other_port(port.direction()) == Some(port) && hugr.is_linked(node, port)
722}
723
724/// Whether node has a non-df linked port in the given direction.
725fn has_other_edge<H: HugrView>(hugr: &H, node: Node, dir: Direction) -> bool {
726    let op = hugr.get_optype(node);
727    op.other_port_kind(dir).is_some() && hugr.is_linked(node, op.other_port(dir).unwrap())
728}
729
730/// Errors that can occur while constructing a [`SimpleReplacement`].
731#[derive(Debug, Clone, PartialEq, Error)]
732#[non_exhaustive]
733pub enum InvalidReplacement {
734    /// No DataflowParent root in replacement graph.
735    #[error("The root of the replacement {node} is a {}, but only OpType::DFGs are supported.", op.name())]
736    InvalidDataflowGraph {
737        /// The node ID of the root node.
738        node: Node,
739        /// The op type of the root node.
740        op: OpType,
741    },
742    /// Replacement graph type mismatch.
743    #[error(
744        "Replacement graph type mismatch. Expected {expected}, got {}.",
745        actual.clone().map_or("none".to_string(), |t| t.to_string()))
746    ]
747    InvalidSignature {
748        /// The expected signature.
749        expected: Signature,
750        /// The actual signature.
751        actual: Option<Signature>,
752    },
753    /// SiblingSubgraph is not convex.
754    #[error("SiblingSubgraph is not convex.")]
755    NonConvexSubgraph,
756}
757
758/// Errors that can occur while constructing a [`SiblingSubgraph`].
759#[derive(Debug, Clone, PartialEq, Eq, Error)]
760#[non_exhaustive]
761pub enum InvalidSubgraph {
762    /// The subgraph is not convex.
763    #[error("The subgraph is not convex.")]
764    NotConvex,
765    /// Not all nodes have the same parent.
766    #[error(
767        "Not a sibling subgraph. {first_node} has parent {}, but {other_node} has parent {}.",
768        first_parent.map_or("None".to_string(), |n| n.to_string()),
769        other_parent.map_or("None".to_string(), |n| n.to_string())
770    )]
771    NoSharedParent {
772        /// The first node.
773        first_node: Node,
774        /// The parent of the first node.
775        first_parent: Option<Node>,
776        /// The other node.
777        other_node: Node,
778        /// The parent of the other node.
779        other_parent: Option<Node>,
780    },
781    /// Empty subgraphs are not supported.
782    #[error("Empty subgraphs are not supported.")]
783    EmptySubgraph,
784    /// An invalid boundary port was found.
785    #[error("Invalid boundary port.")]
786    InvalidBoundary(#[from] InvalidSubgraphBoundary),
787}
788
789/// Errors that can occur while constructing a [`SiblingSubgraph`].
790#[derive(Debug, Clone, PartialEq, Eq, Error)]
791#[non_exhaustive]
792pub enum InvalidSubgraphBoundary {
793    /// A boundary port's node is not in the set of nodes.
794    #[error("(node {0}, port {1}) is in the boundary, but node {0} is not in the set.")]
795    PortNodeNotInSet(Node, Port),
796    /// A boundary port has no connections outside the subgraph.
797    #[error("(node {0}, port {1}) is in the boundary, but the port is not connected to a node outside the subgraph.")]
798    DisconnectedBoundaryPort(Node, Port),
799    /// There's a non-unique input-boundary port.
800    #[error("A port in the input boundary is used multiple times.")]
801    NonUniqueInput,
802    /// There's an empty partition in the input boundary.
803    #[error("A partition in the input boundary is empty.")]
804    EmptyPartition,
805    /// Different types in a partition of the input boundary.
806    #[error("The partition {0} in the input boundary has ports with different types.")]
807    MismatchedTypes(usize),
808}
809
810#[cfg(test)]
811mod tests {
812    use cool_asserts::assert_matches;
813
814    use crate::builder::inout_sig;
815    use crate::hugr::Rewrite;
816    use crate::ops::Const;
817    use crate::std_extensions::arithmetic::float_types::{self, ConstF64};
818    use crate::std_extensions::logic::{self, LogicOp};
819    use crate::type_row;
820    use crate::utils::test_quantum_extension::{self, cx_gate, rz_f64};
821    use crate::{
822        builder::{
823            BuildError, DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, HugrBuilder,
824            ModuleBuilder,
825        },
826        extension::prelude::{bool_t, qb_t},
827        hugr::views::{HierarchyView, SiblingGraph},
828        ops::handle::{DfgID, FuncID, NodeHandle},
829        std_extensions::logic::test::and_op,
830    };
831
832    use super::*;
833
834    impl SiblingSubgraph {
835        /// A sibling subgraph from a HUGR.
836        ///
837        /// The subgraph is given by the sibling graph of the root. If you wish to
838        /// create a subgraph from another root, wrap the argument `region` in a
839        /// [`super::SiblingGraph`].
840        ///
841        /// This will return an [`InvalidSubgraph::EmptySubgraph`] error if the
842        /// subgraph is empty.
843        fn from_sibling_graph(sibling_graph: &impl HugrView) -> Result<Self, InvalidSubgraph> {
844            let root = sibling_graph.root();
845            let nodes = sibling_graph.children(root).collect_vec();
846            if nodes.is_empty() {
847                Err(InvalidSubgraph::EmptySubgraph)
848            } else {
849                Ok(Self {
850                    nodes,
851                    inputs: Vec::new(),
852                    outputs: Vec::new(),
853                })
854            }
855        }
856    }
857
858    /// A Module with a single function from three qubits to three qubits.
859    /// The function applies a CX gate to the first two qubits and a Rz gate (with a constant angle) to the last qubit.
860    fn build_hugr() -> Result<(Hugr, Node), BuildError> {
861        let mut mod_builder = ModuleBuilder::new();
862        let func = mod_builder.declare(
863            "test",
864            Signature::new_endo(vec![qb_t(), qb_t(), qb_t()])
865                .with_extension_delta(ExtensionSet::from_iter([
866                    test_quantum_extension::EXTENSION_ID,
867                    float_types::EXTENSION_ID,
868                ]))
869                .into(),
870        )?;
871        let func_id = {
872            let mut dfg = mod_builder.define_declaration(&func)?;
873            let [w0, w1, w2] = dfg.input_wires_arr();
874            let [w0, w1] = dfg.add_dataflow_op(cx_gate(), [w0, w1])?.outputs_arr();
875            let c = dfg.add_load_const(Const::new(ConstF64::new(0.5).into()));
876            let [w2] = dfg.add_dataflow_op(rz_f64(), [w2, c])?.outputs_arr();
877            dfg.finish_with_outputs([w0, w1, w2])?
878        };
879        let hugr = mod_builder
880            .finish_hugr()
881            .map_err(|e| -> BuildError { e.into() })?;
882        Ok((hugr, func_id.node()))
883    }
884
885    /// A bool to bool hugr with three subsequent NOT gates.
886    fn build_3not_hugr() -> Result<(Hugr, Node), BuildError> {
887        let mut mod_builder = ModuleBuilder::new();
888        let func = mod_builder.declare(
889            "test",
890            Signature::new_endo(vec![bool_t()])
891                .with_extension_delta(logic::EXTENSION_ID)
892                .into(),
893        )?;
894        let func_id = {
895            let mut dfg = mod_builder.define_declaration(&func)?;
896            let outs1 = dfg.add_dataflow_op(LogicOp::Not, dfg.input_wires())?;
897            let outs2 = dfg.add_dataflow_op(LogicOp::Not, outs1.outputs())?;
898            let outs3 = dfg.add_dataflow_op(LogicOp::Not, outs2.outputs())?;
899            dfg.finish_with_outputs(outs3.outputs())?
900        };
901        let hugr = mod_builder
902            .finish_hugr()
903            .map_err(|e| -> BuildError { e.into() })?;
904        Ok((hugr, func_id.node()))
905    }
906
907    /// A bool to (bool, bool) with multiports.
908    fn build_multiport_hugr() -> Result<(Hugr, Node), BuildError> {
909        let mut mod_builder = ModuleBuilder::new();
910        let func = mod_builder.declare(
911            "test",
912            Signature::new(bool_t(), vec![bool_t(), bool_t()])
913                .with_extension_delta(logic::EXTENSION_ID)
914                .into(),
915        )?;
916        let func_id = {
917            let mut dfg = mod_builder.define_declaration(&func)?;
918            let [b0] = dfg.input_wires_arr();
919            let [b1] = dfg.add_dataflow_op(LogicOp::Not, [b0])?.outputs_arr();
920            let [b2] = dfg.add_dataflow_op(LogicOp::Not, [b1])?.outputs_arr();
921            dfg.finish_with_outputs([b1, b2])?
922        };
923        let hugr = mod_builder
924            .finish_hugr()
925            .map_err(|e| -> BuildError { e.into() })?;
926        Ok((hugr, func_id.node()))
927    }
928
929    /// A HUGR with a copy
930    fn build_hugr_classical() -> Result<(Hugr, Node), BuildError> {
931        let mut mod_builder = ModuleBuilder::new();
932        let func = mod_builder.declare(
933            "test",
934            Signature::new_endo(bool_t())
935                .with_extension_delta(logic::EXTENSION_ID)
936                .into(),
937        )?;
938        let func_id = {
939            let mut dfg = mod_builder.define_declaration(&func)?;
940            let in_wire = dfg.input_wires().exactly_one().unwrap();
941            let outs = dfg.add_dataflow_op(and_op(), [in_wire, in_wire])?;
942            dfg.finish_with_outputs(outs.outputs())?
943        };
944        let hugr = mod_builder
945            .finish_hugr()
946            .map_err(|e| -> BuildError { e.into() })?;
947        Ok((hugr, func_id.node()))
948    }
949
950    #[test]
951    fn construct_subgraph() -> Result<(), InvalidSubgraph> {
952        let (hugr, func_root) = build_hugr().unwrap();
953        let sibling_graph: SiblingGraph<'_> = SiblingGraph::try_new(&hugr, func_root).unwrap();
954        let from_root = SiblingSubgraph::from_sibling_graph(&sibling_graph)?;
955        let region: SiblingGraph<'_> = SiblingGraph::try_new(&hugr, func_root).unwrap();
956        let from_region = SiblingSubgraph::from_sibling_graph(&region)?;
957        assert_eq!(
958            from_root.get_parent(&sibling_graph),
959            from_region.get_parent(&sibling_graph)
960        );
961        assert_eq!(
962            from_root.signature(&sibling_graph),
963            from_region.signature(&sibling_graph)
964        );
965        Ok(())
966    }
967
968    #[test]
969    fn construct_simple_replacement() -> Result<(), InvalidSubgraph> {
970        let (mut hugr, func_root) = build_hugr().unwrap();
971        let func: SiblingGraph<'_, FuncID<true>> = SiblingGraph::try_new(&hugr, func_root).unwrap();
972        let sub = SiblingSubgraph::try_new_dataflow_subgraph(&func)?;
973
974        let empty_dfg = {
975            let builder =
976                DFGBuilder::new(Signature::new_endo(vec![qb_t(), qb_t(), qb_t()])).unwrap();
977            let inputs = builder.input_wires();
978            builder.finish_hugr_with_outputs(inputs).unwrap()
979        };
980
981        let rep = sub.create_simple_replacement(&func, empty_dfg).unwrap();
982
983        assert_eq!(rep.subgraph().nodes().len(), 4);
984
985        assert_eq!(hugr.node_count(), 8); // Module + Def + In + CX + Rz + Const + LoadConst + Out
986        hugr.apply_rewrite(rep).unwrap();
987        assert_eq!(hugr.node_count(), 4); // Module + Def + In + Out
988
989        Ok(())
990    }
991
992    #[test]
993    fn test_signature() -> Result<(), InvalidSubgraph> {
994        let (hugr, dfg) = build_hugr().unwrap();
995        let func: SiblingGraph<'_, FuncID<true>> = SiblingGraph::try_new(&hugr, dfg).unwrap();
996        let sub = SiblingSubgraph::try_new_dataflow_subgraph(&func)?;
997        assert_eq!(
998            sub.signature(&func),
999            Signature::new_endo(vec![qb_t(), qb_t(), qb_t()]).with_extension_delta(
1000                ExtensionSet::from_iter([
1001                    test_quantum_extension::EXTENSION_ID,
1002                    float_types::EXTENSION_ID,
1003                ])
1004            )
1005        );
1006        Ok(())
1007    }
1008
1009    #[test]
1010    fn construct_simple_replacement_invalid_signature() -> Result<(), InvalidSubgraph> {
1011        let (hugr, dfg) = build_hugr().unwrap();
1012        let func: SiblingGraph<'_> = SiblingGraph::try_new(&hugr, dfg).unwrap();
1013        let sub = SiblingSubgraph::from_sibling_graph(&func)?;
1014
1015        let empty_dfg = {
1016            let builder = DFGBuilder::new(Signature::new_endo(vec![qb_t()])).unwrap();
1017            let inputs = builder.input_wires();
1018            builder.finish_hugr_with_outputs(inputs).unwrap()
1019        };
1020
1021        assert_matches!(
1022            sub.create_simple_replacement(&func, empty_dfg).unwrap_err(),
1023            InvalidReplacement::InvalidSignature { .. }
1024        );
1025        Ok(())
1026    }
1027
1028    #[test]
1029    fn convex_subgraph() {
1030        let (hugr, func_root) = build_hugr().unwrap();
1031        let func: SiblingGraph<'_, FuncID<true>> = SiblingGraph::try_new(&hugr, func_root).unwrap();
1032        assert_eq!(
1033            SiblingSubgraph::try_new_dataflow_subgraph(&func)
1034                .unwrap()
1035                .nodes()
1036                .len(),
1037            4
1038        )
1039    }
1040
1041    #[test]
1042    fn convex_subgraph_2() {
1043        let (hugr, func_root) = build_hugr().unwrap();
1044        let [inp, out] = hugr.get_io(func_root).unwrap();
1045        let func: SiblingGraph<'_> = SiblingGraph::try_new(&hugr, func_root).unwrap();
1046        // All graph except input/output nodes
1047        SiblingSubgraph::try_new(
1048            hugr.node_outputs(inp)
1049                .take(2)
1050                .map(|p| hugr.linked_inputs(inp, p).collect_vec())
1051                .filter(|ps| !ps.is_empty())
1052                .collect(),
1053            hugr.node_inputs(out)
1054                .take(2)
1055                .filter_map(|p| hugr.single_linked_output(out, p))
1056                .collect(),
1057            &func,
1058        )
1059        .unwrap();
1060    }
1061
1062    #[test]
1063    fn degen_boundary() {
1064        let (hugr, func_root) = build_hugr().unwrap();
1065        let func: SiblingGraph<'_> = SiblingGraph::try_new(&hugr, func_root).unwrap();
1066        let [inp, _] = hugr.get_io(func_root).unwrap();
1067        let first_cx_edge = hugr.node_outputs(inp).next().unwrap();
1068        // All graph but one edge
1069        assert_matches!(
1070            SiblingSubgraph::try_new(
1071                vec![hugr
1072                    .linked_ports(inp, first_cx_edge)
1073                    .map(|(n, p)| (n, p.as_incoming().unwrap()))
1074                    .collect()],
1075                vec![(inp, first_cx_edge)],
1076                &func,
1077            ),
1078            Err(InvalidSubgraph::InvalidBoundary(
1079                InvalidSubgraphBoundary::DisconnectedBoundaryPort(_, _)
1080            ))
1081        );
1082    }
1083
1084    #[test]
1085    fn non_convex_subgraph() {
1086        let (hugr, func_root) = build_3not_hugr().unwrap();
1087        let func: SiblingGraph<'_> = SiblingGraph::try_new(&hugr, func_root).unwrap();
1088        let [inp, _out] = hugr.get_io(func_root).unwrap();
1089        let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap();
1090        let not2 = hugr.output_neighbours(not1).exactly_one().ok().unwrap();
1091        let not3 = hugr.output_neighbours(not2).exactly_one().ok().unwrap();
1092        let not1_inp = hugr.node_inputs(not1).next().unwrap();
1093        let not1_out = hugr.node_outputs(not1).next().unwrap();
1094        let not3_inp = hugr.node_inputs(not3).next().unwrap();
1095        let not3_out = hugr.node_outputs(not3).next().unwrap();
1096        assert_matches!(
1097            SiblingSubgraph::try_new(
1098                vec![vec![(not1, not1_inp)], vec![(not3, not3_inp)]],
1099                vec![(not1, not1_out), (not3, not3_out)],
1100                &func
1101            ),
1102            Err(InvalidSubgraph::NotConvex)
1103        );
1104    }
1105
1106    /// A subgraphs mixed with multiports caused a NonConvex error.
1107    /// https://github.com/CQCL/hugr/issues/1294
1108    #[test]
1109    fn convex_multiports() {
1110        let (hugr, func_root) = build_multiport_hugr().unwrap();
1111        let [inp, out] = hugr.get_io(func_root).unwrap();
1112        let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap();
1113        let not2 = hugr
1114            .output_neighbours(not1)
1115            .filter(|&n| n != out)
1116            .exactly_one()
1117            .ok()
1118            .unwrap();
1119
1120        let subgraph = SiblingSubgraph::try_from_nodes([not1, not2], &hugr).unwrap();
1121        assert_eq!(subgraph.nodes(), [not1, not2]);
1122    }
1123
1124    #[test]
1125    fn invalid_boundary() {
1126        let (hugr, func_root) = build_hugr().unwrap();
1127        let func: SiblingGraph<'_> = SiblingGraph::try_new(&hugr, func_root).unwrap();
1128        let [inp, out] = hugr.get_io(func_root).unwrap();
1129        let cx_edges_in = hugr.node_outputs(inp);
1130        let cx_edges_out = hugr.node_inputs(out);
1131        // All graph but the CX
1132        assert_matches!(
1133            SiblingSubgraph::try_new(
1134                cx_edges_out.map(|p| vec![(out, p)]).collect(),
1135                cx_edges_in.map(|p| (inp, p)).collect(),
1136                &func,
1137            ),
1138            Err(InvalidSubgraph::InvalidBoundary(
1139                InvalidSubgraphBoundary::DisconnectedBoundaryPort(_, _)
1140            ))
1141        );
1142    }
1143
1144    #[test]
1145    fn preserve_signature() {
1146        let (hugr, func_root) = build_hugr_classical().unwrap();
1147        let func_graph: SiblingGraph<'_, FuncID<true>> =
1148            SiblingGraph::try_new(&hugr, func_root).unwrap();
1149        let func = SiblingSubgraph::try_new_dataflow_subgraph(&func_graph).unwrap();
1150        let func_defn = hugr.get_optype(func_root).as_func_defn().unwrap();
1151        assert_eq!(func_defn.signature, func.signature(&func_graph).into());
1152    }
1153
1154    #[test]
1155    fn extract_subgraph() {
1156        let (hugr, func_root) = build_hugr().unwrap();
1157        let func_graph: SiblingGraph<'_, FuncID<true>> =
1158            SiblingGraph::try_new(&hugr, func_root).unwrap();
1159        let subgraph = SiblingSubgraph::try_new_dataflow_subgraph(&func_graph).unwrap();
1160        let extracted = subgraph.extract_subgraph(&hugr, "region");
1161
1162        extracted.validate().unwrap();
1163    }
1164
1165    #[test]
1166    fn edge_both_output_and_copy() {
1167        // https://github.com/CQCL/hugr/issues/518
1168        let one_bit = vec![bool_t()];
1169        let two_bit = vec![bool_t(), bool_t()];
1170
1171        let mut builder = DFGBuilder::new(inout_sig(one_bit.clone(), two_bit.clone())).unwrap();
1172        let inw = builder.input_wires().exactly_one().unwrap();
1173        let outw1 = builder
1174            .add_dataflow_op(LogicOp::Not, [inw])
1175            .unwrap()
1176            .out_wire(0);
1177        let outw2 = builder
1178            .add_dataflow_op(and_op(), [inw, outw1])
1179            .unwrap()
1180            .outputs();
1181        let outw = [outw1].into_iter().chain(outw2);
1182        let h = builder.finish_hugr_with_outputs(outw).unwrap();
1183        let view = SiblingGraph::<DfgID>::try_new(&h, h.root()).unwrap();
1184        let subg = SiblingSubgraph::try_new_dataflow_subgraph(&view).unwrap();
1185        assert_eq!(subg.nodes().len(), 2);
1186    }
1187
1188    #[test]
1189    fn test_unconnected() {
1190        // test a replacement on a subgraph with a discarded output
1191        let mut b = DFGBuilder::new(
1192            Signature::new(bool_t(), type_row![])
1193                // .with_prelude()
1194                .with_extension_delta(crate::std_extensions::logic::EXTENSION_ID),
1195        )
1196        .unwrap();
1197        let inw = b.input_wires().exactly_one().unwrap();
1198        let not_n = b.add_dataflow_op(LogicOp::Not, [inw]).unwrap();
1199        // Unconnected output, discarded
1200        let mut h = b.finish_hugr_with_outputs([]).unwrap();
1201
1202        let subg = SiblingSubgraph::from_node(not_n.node(), &h);
1203
1204        assert_eq!(subg.nodes().len(), 1);
1205        //  TODO create a valid replacement
1206        let replacement = {
1207            let mut rep_b = DFGBuilder::new(
1208                Signature::new_endo(bool_t())
1209                    .with_extension_delta(crate::std_extensions::logic::EXTENSION_ID),
1210            )
1211            .unwrap();
1212            let inw = rep_b.input_wires().exactly_one().unwrap();
1213
1214            let not_n = rep_b.add_dataflow_op(LogicOp::Not, [inw]).unwrap();
1215
1216            rep_b.finish_hugr_with_outputs(not_n.outputs()).unwrap()
1217        };
1218        let rep = subg.create_simple_replacement(&h, replacement).unwrap();
1219        rep.apply(&mut h).unwrap();
1220    }
1221}