hugr_core/hugr/
persistent.rs

1//! Persistent data structure for HUGR mutations.
2//!
3//! This module provides a persistent data structure [`PersistentHugr`] that
4//! implements [`crate::HugrView`]; mutations to the data are stored
5//! persistently as a set of [`Commit`]s along with the dependencies between the
6//! commits.
7//!
8//! As a result of persistency, the entire mutation history of a HUGR can be
9//! traversed and references to previous versions of the data remain valid even
10//! as the HUGR graph is "mutated" by applying patches: the patches are in
11//! effect added to the history as new commits.
12//!
13//! The data structure underlying [`PersistentHugr`], which stores the history
14//! of all commits, is [`CommitStateSpace`]. Multiple [`PersistentHugr`] can be
15//! stored within a single [`CommitStateSpace`], which allows for the efficient
16//! exploration of the space of all possible graph rewrites.
17//!
18//! ## Overlapping commits
19//!
20//! In general, [`CommitStateSpace`] may contain overlapping commits. Such
21//! mutations are mutually exclusive as they modify the same nodes. It is
22//! therefore not possible to apply all commits in a [`CommitStateSpace`]
23//! simultaneously. A [`PersistentHugr`] on the other hand always corresponds to
24//! a subgraph of a [`CommitStateSpace`] that is guaranteed to contain only
25//! non-overlapping, compatible commits. By applying all commits in a
26//! [`PersistentHugr`], we can materialize a [`Hugr`]. Traversing the
27//! materialized HUGR is equivalent to using the [`crate::HugrView`]
28//! implementation of the corresponding [`PersistentHugr`].
29//!
30//! ## Summary of data types
31//!
32//! - [`Commit`] A modification to a [`Hugr`] (currently a
33//!   [`SimpleReplacement`]) that forms the atomic unit of change for a
34//!   [`PersistentHugr`] (like a commit in git). This is a reference-counted
35//!   value that is cheap to clone and will be freed when the last reference is
36//!   dropped.
37//! - [`PersistentHugr`] A data structure that implements [`crate::HugrView`]
38//!   and can be used as a drop-in replacement for a [`crate::Hugr`] for
39//!   read-only access and mutations through the [`PatchVerification`] and
40//!   [`Patch`] traits. Mutations are stored as a history of commits. Unlike
41//!   [`CommitStateSpace`], it maintains the invariant that all contained
42//!   commits are compatible with eachother.
43//! - [`CommitStateSpace`] Stores commits, recording the dependencies between
44//!   them. Includes the base HUGR and any number of possibly incompatible
45//!   (overlapping) commits. Unlike a [`PersistentHugr`], a state space can
46//!   contain mutually exclusive commits.
47//!
48//! ## Usage
49//!
50//! A [`PersistentHugr`] can be created from a base HUGR using
51//! [`PersistentHugr::with_base`]. Replacements can then be applied to it
52//! using [`PersistentHugr::add_replacement`]. Alternatively, if you already
53//! have a populated state space, use [`PersistentHugr::try_new`] to create a
54//! new HUGR with those commits.
55//!
56//! Add a sequence of commits to a state space by merging a [`PersistentHugr`]
57//! into it using [`CommitStateSpace::extend`] or directly using
58//! [`CommitStateSpace::try_add_commit`].
59//!
60//! To obtain a [`PersistentHugr`] from your state space, use
61//! [`CommitStateSpace::try_extract_hugr`]. A [`PersistentHugr`] can always be
62//! materialized into a [`Hugr`] type using [`PersistentHugr::to_hugr`].
63
64mod parents_view;
65mod resolver;
66mod state_space;
67mod trait_impls;
68pub mod walker;
69
70pub use walker::{PinnedWire, Walker};
71
72use std::{
73    collections::{BTreeSet, HashMap, VecDeque},
74    mem, vec,
75};
76
77use delegate::delegate;
78use derive_more::derive::From;
79use itertools::{Either, Itertools};
80use relrc::RelRc;
81use state_space::CommitData;
82pub use state_space::{CommitId, CommitStateSpace, InvalidCommit, PatchNode};
83
84pub use resolver::PointerEqResolver;
85
86use crate::{
87    Hugr, HugrView, IncomingPort, Node, OutgoingPort, Port, SimpleReplacement,
88    hugr::patch::{Patch, PatchVerification, simple_replace},
89};
90
91/// A replacement operation that can be applied to a [`PersistentHugr`].
92pub type PersistentReplacement = SimpleReplacement<PatchNode>;
93
94/// A patch that can be applied to a [`PersistentHugr`] or a
95/// [`CommitStateSpace`] as an atomic commit.
96///
97/// Commits are cheap to clone: they are reference-counted pointers to the
98/// patch data. They also maintain strong references to the ancestor commits
99/// that the patch may depend on (i.e. other patches that must be applied
100/// before `self` can be applied).
101///
102/// Currently, patches must be [`SimpleReplacement`]s.
103#[derive(Debug, Clone, From)]
104#[repr(transparent)]
105pub struct Commit(RelRc<CommitData, ()>);
106
107impl Commit {
108    /// Create a commit from a simple replacement.
109    ///
110    /// Requires a reference to the commit state space that the nodes in
111    /// `replacement` refer to.
112    ///
113    /// The replacement must act on a non-empty subgraph, otherwise this
114    /// function will return an [`InvalidCommit::EmptyReplacement`] error.
115    ///
116    /// If any of the parents of the replacement are not in the commit state
117    /// space, this function will return an [`InvalidCommit::UnknownParent`]
118    /// error.
119    pub fn try_from_replacement(
120        replacement: PersistentReplacement,
121        graph: &CommitStateSpace,
122    ) -> Result<Commit, InvalidCommit> {
123        if replacement.subgraph().nodes().is_empty() {
124            return Err(InvalidCommit::EmptyReplacement);
125        }
126        let parent_ids = replacement.invalidation_set().map(|n| n.0).unique();
127        let parents = parent_ids
128            .map(|id| {
129                if graph.contains_id(id) {
130                    Ok(graph.get_commit(id).clone())
131                } else {
132                    Err(InvalidCommit::UnknownParent(id))
133                }
134            })
135            .collect::<Result<Vec<_>, _>>()?;
136        let rc = RelRc::with_parents(
137            replacement.into(),
138            parents.into_iter().map(|p| (p.into(), ())),
139        );
140        Ok(Self(rc))
141    }
142
143    fn as_relrc(&self) -> &RelRc<CommitData, ()> {
144        &self.0
145    }
146
147    /// Get the set of nodes inserted by the patch in `self`.
148    pub fn inserted_nodes(&self) -> impl Iterator<Item = Node> + '_ {
149        match self.0.value() {
150            CommitData::Base(base) => Either::Left(base.nodes()),
151            CommitData::Replacement(repl) => {
152                // Skip the entrypoint and the IO nodes
153                Either::Right(repl.replacement().entry_descendants().skip(3))
154            }
155        }
156    }
157
158    fn all_parents(&self) -> impl Iterator<Item = &Commit> + '_ {
159        self.0.all_parents().map_into()
160    }
161
162    /// Get the patch that `self` represents.
163    pub fn replacement(&self) -> Option<&PersistentReplacement> {
164        match self.0.value() {
165            CommitData::Base(_) => None,
166            CommitData::Replacement(replacement) => Some(replacement),
167        }
168    }
169
170    /// Get the set of nodes invalidated by the patch in `self`.
171    pub fn invalidation_set(&self) -> impl Iterator<Item = PatchNode> + '_ {
172        self.replacement()
173            .into_iter()
174            .flat_map(|r| r.invalidation_set())
175    }
176
177    /// Get the set of nodes deleted by applying `self`.
178    pub fn deleted_nodes(&self) -> impl Iterator<Item = PatchNode> + '_ {
179        self.replacement()
180            .into_iter()
181            .flat_map(|r| r.subgraph().nodes())
182            .copied()
183    }
184
185    delegate! {
186        to self.0 {
187            fn value(&self) -> &CommitData;
188            fn as_ptr(&self) -> *const relrc::node::InnerData<CommitData, ()>;
189        }
190    }
191
192    /// Get all ancestors of `self` in reverse topological order, up until and
193    /// including the first commit for which `continue_fn` returns false.
194    fn get_ancestors_while<'a>(
195        &'a self,
196        continue_fn: impl Fn(&'a Commit) -> bool,
197    ) -> Vec<&'a Commit> {
198        let mut next_ancestor = 0;
199        let mut ancestors = vec![self];
200        let mut seen = BTreeSet::from_iter([self.as_ptr()]);
201        while next_ancestor < ancestors.len() {
202            let commit = &ancestors[next_ancestor];
203            next_ancestor += 1;
204            if !continue_fn(commit) {
205                continue;
206            }
207            for parent in commit.all_parents() {
208                if seen.insert(parent.as_ptr()) {
209                    ancestors.push(parent);
210                }
211            }
212        }
213        ancestors
214    }
215}
216
217impl From<Commit> for RelRc<CommitData, ()> {
218    fn from(Commit(data): Commit) -> Self {
219        data
220    }
221}
222
223impl<'a> From<&'a RelRc<CommitData, ()>> for &'a Commit {
224    fn from(rc: &'a RelRc<CommitData, ()>) -> Self {
225        // SAFETY: Commit is a transparent wrapper around RelRc
226        unsafe { mem::transmute(rc) }
227    }
228}
229
230/// A HUGR-like object that tracks its mutation history.
231///
232/// When mutations are applied to a [`PersistentHugr`], the object is mutated
233/// as expected but all references to previous versions of the object remain
234/// valid. Furthermore, older versions of the data can be recovered by
235/// traversing the object's history with [`Self::as_state_space`].
236///
237/// Multiple references to various versions of a Hugr can be maintained in
238/// parallel by extracting them from a shared [`CommitStateSpace`].
239///
240/// ## Supported access and mutation
241///
242/// [`PersistentHugr`] implements [`crate::HugrView`], so that it can used as
243/// a drop-in substitute for a Hugr wherever read-only access is required. It
244/// does not implement [`HugrMut`](crate::hugr::HugrMut), however. Mutations
245/// must be performed by applying patches (see [`PatchVerification`] and
246/// [`Patch`]). Currently, only [`SimpleReplacement`] patches are supported. You
247/// can use [`Self::add_replacement`] to add a patch to `self`, or use the
248/// aforementioned patch traits.
249///
250/// ## Patches, commits and history
251///
252/// A [`PersistentHugr`] is composed of a unique base HUGR, along with a set of
253/// mutations applied to it. All mutations are stored in the form of commits
254/// that store the patches applied on top of a base HUGR. You may think of it
255/// as a "queue" of patches: whenever the patch of a commit is "applied", it is
256/// in reality just added to the queue. In practice, the total order of the
257/// queue is irrelevant, as patches only depend on a subset of the previously
258/// applied patches. This creates a partial order on the commits: a directed
259/// acyclic graph that we call the "commit history". A commit history is in
260/// effect a subgraph of a commit state space, with the additional invariant
261/// that all commits within the history are compatible.
262///
263/// ## Supported graph types
264///
265/// Currently, only patches that apply to subgraphs within dataflow regions
266/// are supported.
267#[derive(Clone, Debug)]
268pub struct PersistentHugr {
269    /// The state space of all commits.
270    ///
271    /// Invariant: all commits are "compatible", meaning that no two patches
272    /// invalidate the same node.
273    state_space: CommitStateSpace,
274}
275
276impl PersistentHugr {
277    /// Create a [`PersistentHugr`] with `hugr` as its base HUGR.
278    ///
279    /// All replacements added in the future will apply on top of `hugr`.
280    pub fn with_base(hugr: Hugr) -> Self {
281        let state_space = CommitStateSpace::with_base(hugr);
282        Self { state_space }
283    }
284
285    /// Create a [`PersistentHugr`] from a single commit and its ancestors.
286    // This always defines a valid `PersistentHugr` as the ancestors of a commit
287    // are guaranteed to be compatible with each other.
288    pub fn from_commit(commit: Commit) -> Self {
289        let state_space = CommitStateSpace::try_from_commits([commit]).expect("commit is valid");
290        Self { state_space }
291    }
292
293    /// Create a [`PersistentHugr`] from a list of commits.
294    ///
295    /// `Self` will correspond to the HUGR obtained by applying the patches of
296    /// the given commits and of all their ancestors.
297    ///
298    /// If the state space of the commits would include two commits which are
299    /// incompatible, or if the commits do not share a common base HUGR, then
300    /// an error is returned.
301    pub fn try_new(commits: impl IntoIterator<Item = Commit>) -> Result<Self, InvalidCommit> {
302        let graph = CommitStateSpace::try_from_commits(commits)?;
303        graph.try_extract_hugr(graph.all_commit_ids())
304    }
305
306    /// Construct a [`PersistentHugr`] from a [`CommitStateSpace`].
307    ///
308    /// Does not check that the commits are compatible.
309    fn from_state_space_unsafe(state_space: CommitStateSpace) -> Self {
310        Self { state_space }
311    }
312
313    /// Add a replacement to `self`.
314    ///
315    /// The effect of this is equivalent to applying `replacement` to the
316    /// equivalent HUGR, i.e. `self.to_hugr().apply(replacement)` is
317    /// equivalent to `self.add_replacement(replacement).to_hugr()`.
318    ///
319    /// This will panic if the replacement is invalid. Use
320    /// [`PersistentHugr::try_add_replacement`] instead for more graceful error
321    /// handling.
322    pub fn add_replacement(&mut self, replacement: PersistentReplacement) -> CommitId {
323        self.try_add_replacement(replacement)
324            .expect("invalid replacement")
325    }
326
327    /// Add a replacement to `self`, with error handling.
328    ///
329    /// All parent commits must already be in `self`.
330    ///
331    /// Return the ID of) the commit if it was added successfully. This may
332    /// return the following errors:
333    /// - a [`InvalidCommit::IncompatibleHistory`] error if the replacement is
334    ///   incompatible with another commit already in `self`, or
335    /// - a [`InvalidCommit::UnknownParent`] error if one of the commits that
336    ///   `replacement` applies on top of is not contained in `self`.
337    pub fn try_add_replacement(
338        &mut self,
339        replacement: PersistentReplacement,
340    ) -> Result<CommitId, InvalidCommit> {
341        // Check that `replacement` does not conflict with siblings at any of its
342        // parents
343        let new_invalid_nodes = replacement
344            .subgraph()
345            .nodes()
346            .iter()
347            .map(|&PatchNode(id, node)| (id, node))
348            .into_grouping_map()
349            .collect::<BTreeSet<_>>();
350        for (parent, new_invalid_nodes) in new_invalid_nodes {
351            let invalidation_set = self.invalidation_set(parent).collect();
352            if let Some(&node) = new_invalid_nodes.intersection(&invalidation_set).next() {
353                return Err(InvalidCommit::IncompatibleHistory(parent, node));
354            }
355        }
356
357        self.state_space.try_add_replacement(replacement)
358    }
359
360    /// Add a commit to `self` and all its ancestors.
361    ///
362    /// The commit and all its ancestors must be compatible with all existing
363    /// commits in `self`. If this is not satisfied, an
364    /// [`InvalidCommit::IncompatibleHistory`] error is returned. In this case,
365    /// as many compatible commits as possible are added to `self`.
366    pub fn try_add_commit(&mut self, commit: Commit) -> Result<CommitId, InvalidCommit> {
367        let new_commits = commit.get_ancestors_while(|c| !self.contains(c));
368        let mut commit_id = None;
369        for &commit in new_commits.iter().rev() {
370            commit_id = Some(self.state_space.try_add_commit(commit.clone())?);
371            let commit_id = commit_id.unwrap();
372
373            // Check that the new commit is compatible with all its (current and
374            // future) children
375            let curr_children = self
376                .state_space
377                .children(commit_id)
378                .map(|id| self.get_commit(id));
379            let new_children = new_commits
380                .iter()
381                .copied()
382                .filter(|c| c.all_parents().any(|p| p.as_ptr() == commit.as_ptr()));
383            if let Some(node) = find_conflicting_node(
384                commit_id,
385                curr_children.chain(new_children).unique_by(|c| c.as_ptr()),
386            ) {
387                return Err(InvalidCommit::IncompatibleHistory(commit_id, node));
388            }
389        }
390        Ok(commit_id.expect("new_commits cannot be empty"))
391    }
392
393    /// Convert this `PersistentHugr` to a materialized Hugr by applying all
394    /// commits in `self`.
395    ///
396    /// This operation may be expensive and should be avoided in
397    /// performance-critical paths. For read-only views into the data, rely
398    /// instead on the [`crate::HugrView`] implementation when possible.
399    pub fn to_hugr(&self) -> Hugr {
400        self.apply_all().0
401    }
402
403    /// Apply all commits in `self` to the base HUGR.
404    ///
405    /// Also returns a map from the nodes of the base HUGR to the nodes of the
406    /// materialized HUGR.
407    pub fn apply_all(&self) -> (Hugr, HashMap<PatchNode, Node>) {
408        let mut hugr = self.state_space.base_hugr().clone();
409        let mut node_map = HashMap::from_iter(
410            hugr.nodes()
411                .map(|n| (PatchNode(self.state_space.base(), n), n)),
412        );
413        for commit_id in self.toposort_commits() {
414            let Some(repl) = self.state_space.get_commit(commit_id).replacement() else {
415                continue;
416            };
417
418            let repl = repl.map_host_nodes(|n| node_map[&n]);
419
420            let simple_replace::Outcome {
421                node_map: new_node_map,
422                removed_nodes,
423            } = repl.apply(&mut hugr).expect("invalid replacement");
424
425            debug_assert!(
426                hugr.validate().is_ok(),
427                "malformed patch in persistent hugr:\n{}",
428                hugr.mermaid_string()
429            );
430
431            for (old_node, new_node) in new_node_map {
432                let old_patch_node = PatchNode(commit_id, old_node);
433                node_map.insert(old_patch_node, new_node);
434            }
435            for remove_node in removed_nodes.into_keys() {
436                let &remove_patch_node = node_map
437                    .iter()
438                    .find_map(|(patch_node, &hugr_node)| {
439                        (hugr_node == remove_node).then_some(patch_node)
440                    })
441                    .expect("node not found in node_map");
442                node_map.remove(&remove_patch_node);
443            }
444        }
445        (hugr, node_map)
446    }
447
448    /// Get a reference to the underlying state space of `self`.
449    pub fn as_state_space(&self) -> &CommitStateSpace {
450        &self.state_space
451    }
452
453    /// Convert `self` into its underlying [`CommitStateSpace`].
454    pub fn into_state_space(self) -> CommitStateSpace {
455        self.state_space
456    }
457
458    /// The unique outgoing port in `self` that `port` is attached to.
459    ///
460    /// # Panics
461    ///
462    /// Panics if `node` is not in `self` (in particular if it is deleted) or if
463    /// `port` is not a value port in `node`.
464    fn get_single_outgoing_port(
465        &self,
466        node: PatchNode,
467        port: impl Into<IncomingPort>,
468    ) -> (PatchNode, OutgoingPort) {
469        let mut in_port = port.into();
470        let PatchNode(commit_id, mut in_node) = node;
471
472        assert!(self.is_value_port(node, in_port), "not a dataflow wire");
473        assert!(self.contains_node(node), "node not in self");
474
475        let hugr = self.commit_hugr(commit_id);
476        let (mut out_node, mut out_port) = hugr
477            .single_linked_output(in_node, in_port)
478            .map(|(n, p)| (PatchNode(commit_id, n), p))
479            .expect("invalid HUGR");
480
481        // invariant: (out_node, out_port) -> (in_node, in_port) is a boundary
482        // edge, i.e. it never is the case that both are deleted by the same
483        // child commit
484        loop {
485            let commit_id = out_node.0;
486
487            if let Some(deleted_by) = self.find_deleting_commit(out_node) {
488                (out_node, out_port) = self
489                    .state_space
490                    .linked_child_output(PatchNode(commit_id, in_node), in_port, deleted_by)
491                    .expect("valid boundary edge");
492                // update (in_node, in_port)
493                (in_node, in_port) = {
494                    let new_commit_id = out_node.0;
495                    let hugr = self.commit_hugr(new_commit_id);
496                    hugr.linked_inputs(out_node.1, out_port)
497                        .find(|&(n, _)| {
498                            self.find_deleting_commit(PatchNode(commit_id, n)).is_none()
499                        })
500                        .expect("out_node is connected to output node (which is never deleted)")
501                };
502            } else if self
503                .replacement(commit_id)
504                .is_some_and(|repl| repl.get_replacement_io()[0] == out_node.1)
505            {
506                // out_node is an input node
507                (out_node, out_port) = self
508                    .as_state_space()
509                    .linked_parent_input(PatchNode(commit_id, in_node), in_port);
510                // update (in_node, in_port)
511                (in_node, in_port) = {
512                    let new_commit_id = out_node.0;
513                    let hugr = self.commit_hugr(new_commit_id);
514                    hugr.linked_inputs(out_node.1, out_port)
515                        .find(|&(n, _)| {
516                            self.find_deleting_commit(PatchNode(new_commit_id, n))
517                                == Some(commit_id)
518                        })
519                        .expect("boundary edge must connect out_node to deleted node")
520                };
521            } else {
522                // valid outgoing node!
523                return (out_node, out_port);
524            }
525        }
526    }
527
528    /// All incoming ports that the given outgoing port is attached to.
529    ///
530    /// # Panics
531    ///
532    /// Panics if `out_node` is not in `self` (in particular if it is deleted)
533    /// or if `out_port` is not a value port in `out_node`.
534    fn get_all_incoming_ports(
535        &self,
536        out_node: PatchNode,
537        out_port: OutgoingPort,
538    ) -> impl Iterator<Item = (PatchNode, IncomingPort)> {
539        assert!(
540            self.is_value_port(out_node, out_port),
541            "not a dataflow wire"
542        );
543        assert!(self.contains_node(out_node), "node not in self");
544
545        let mut visited = BTreeSet::new();
546        // enqueue the outport and initialise the set of valid incoming ports
547        // to the valid incoming ports in this commit
548        let mut queue = VecDeque::from([(out_node, out_port)]);
549        let mut valid_incoming_ports = BTreeSet::from_iter(
550            self.commit_hugr(out_node.0)
551                .linked_inputs(out_node.1, out_port)
552                .map(|(in_node, in_port)| (PatchNode(out_node.0, in_node), in_port))
553                .filter(|(in_node, _)| self.contains_node(*in_node)),
554        );
555
556        // A simple BFS across the commit history to find all equivalent incoming ports.
557        while let Some((out_node, out_port)) = queue.pop_front() {
558            if !visited.insert((out_node, out_port)) {
559                continue;
560            }
561            let commit_id = out_node.0;
562            let hugr = self.commit_hugr(commit_id);
563            let out_deleted_by = self.find_deleting_commit(out_node);
564            let curr_repl_out = {
565                let repl = self.replacement(commit_id);
566                repl.map(|r| r.get_replacement_io()[1])
567            };
568            // incoming ports are of interest to us if
569            //  (i) they are connected to the output of a replacement (then there will be a
570            //      linked port in a parent commit), or
571            //  (ii) they are deleted by a child commit and are not equal to the out_node
572            //      (then there will be a linked port in a child commit)
573            let is_linked_to_output = curr_repl_out.is_some_and(|curr_repl_out| {
574                hugr.linked_inputs(out_node.1, out_port)
575                    .any(|(in_node, _)| in_node == curr_repl_out)
576            });
577
578            let deleted_by_child: BTreeSet<_> = hugr
579                .linked_inputs(out_node.1, out_port)
580                .filter(|(in_node, _)| Some(in_node) != curr_repl_out.as_ref())
581                .filter_map(|(in_node, _)| {
582                    self.find_deleting_commit(PatchNode(commit_id, in_node))
583                        .filter(|other_deleted_by|
584                                // (out_node, out_port) -> (in_node, in_port) is a boundary edge
585                                // into the child commit `other_deleted_by`
586                                (Some(other_deleted_by) != out_deleted_by.as_ref()))
587                })
588                .collect();
589
590            // Convert an incoming port to the unique outgoing port that it is linked to
591            let to_outgoing_port = |(PatchNode(commit_id, in_node), in_port)| {
592                let hugr = self.commit_hugr(commit_id);
593                let (out_node, out_port) = hugr
594                    .single_linked_output(in_node, in_port)
595                    .expect("valid dfg wire");
596                (PatchNode(commit_id, out_node), out_port)
597            };
598
599            if is_linked_to_output {
600                // Traverse boundary to parent(s)
601                let new_ins = self
602                    .as_state_space()
603                    .linked_parent_outputs(out_node, out_port);
604                for (in_node, in_port) in new_ins {
605                    if self.contains_node(in_node) {
606                        valid_incoming_ports.insert((in_node, in_port));
607                    }
608                    queue.push_back(to_outgoing_port((in_node, in_port)));
609                }
610            }
611
612            for child in deleted_by_child {
613                // Traverse boundary to `child`
614                let new_ins = self
615                    .as_state_space()
616                    .linked_child_inputs(out_node, out_port, child);
617                for (in_node, in_port) in new_ins {
618                    if self.contains_node(in_node) {
619                        valid_incoming_ports.insert((in_node, in_port));
620                    }
621                    queue.push_back(to_outgoing_port((in_node, in_port)));
622                }
623            }
624        }
625
626        valid_incoming_ports.into_iter()
627    }
628
629    delegate! {
630        to self.state_space {
631            /// Check if `commit` is in the PersistentHugr.
632            pub fn contains(&self, commit: &Commit) -> bool;
633            /// Check if `commit_id` is in the PersistentHugr.
634            pub fn contains_id(&self, commit_id: CommitId) -> bool;
635            /// Get the base commit ID.
636            pub fn base(&self) -> CommitId;
637            /// Get the base [`Hugr`].
638            pub fn base_hugr(&self) -> &Hugr;
639            /// Get the base commit.
640            pub fn base_commit(&self) -> &Commit;
641            /// Get the commit with ID `commit_id`.
642            pub fn get_commit(&self, commit_id: CommitId) -> &Commit;
643            /// Get an iterator over all nodes inserted by `commit_id`.
644            ///
645            /// All nodes will be PatchNodes with commit ID `commit_id`.
646            pub fn inserted_nodes(&self, commit_id: CommitId) -> impl Iterator<Item = PatchNode> + '_;
647            /// Get the replacement for `commit_id`.
648            fn replacement(&self, commit_id: CommitId) -> Option<&SimpleReplacement<PatchNode>>;
649            /// Get the Hugr inserted by `commit_id`.
650            ///
651            /// This is either the replacement Hugr of a [`CommitData::Replacement`] or
652            /// the base Hugr of a [`CommitData::Base`].
653            pub(super) fn commit_hugr(&self, commit_id: CommitId) -> &Hugr;
654            /// Get an iterator over all commit IDs in the persistent HUGR.
655            pub fn all_commit_ids(&self) -> impl Iterator<Item = CommitId> + Clone + '_;
656        }
657    }
658
659    /// Get all commits in `self` in topological order.
660    fn toposort_commits(&self) -> Vec<CommitId> {
661        petgraph::algo::toposort(self.state_space.as_history_graph(), None)
662            .expect("history is a DAG")
663    }
664
665    /// Get the set of nodes of `commit_id` that are invalidated by the patches
666    /// in the children commits of `commit_id`.
667    ///
668    /// The invalidation set must include all nodes that are deleted by the
669    /// children commits (as returned by [`Self::deleted_nodes`]), but may
670    /// also include further nodes to enforce stricter exclusivity constraints
671    /// between patches.
672    pub fn invalidation_set(&self, commit_id: CommitId) -> impl Iterator<Item = Node> + '_ {
673        let children = self.state_space.children(commit_id);
674        children
675            .flat_map(move |child_id| self.state_space.invalidation_set(child_id, commit_id))
676            .unique()
677    }
678
679    /// Get the set of nodes of `commit_id` that are deleted by applying
680    /// the children commits of `commit_id`.
681    ///
682    /// This is a subset of [`Self::invalidation_set`]. Whilst the latter is
683    /// used to establish exclusivity constraints between patches, this method
684    /// is used when we are computing the set of nodes currently present in
685    /// `self`.
686    pub fn deleted_nodes(&self, commit_id: CommitId) -> impl Iterator<Item = Node> + '_ {
687        let children = self.state_space.children(commit_id);
688        children
689            .flat_map(move |child_id| {
690                let child = self.get_commit(child_id);
691                child
692                    .deleted_nodes()
693                    .filter_map(move |PatchNode(id, node)| (commit_id == id).then_some(node))
694            })
695            .unique()
696    }
697
698    fn find_deleting_commit(&self, node @ PatchNode(commit_id, _): PatchNode) -> Option<CommitId> {
699        let mut children = self.state_space.children(commit_id);
700        children.find(move |&child_id| {
701            let child = self.get_commit(child_id);
702            child.deleted_nodes().contains(&node)
703        })
704    }
705
706    /// Check if a patch node is in the PersistentHugr, that is, it belongs to
707    /// a commit in the state space and is not deleted by any child commit.
708    pub fn contains_node(&self, PatchNode(commit_id, node): PatchNode) -> bool {
709        let is_replacement_io = || {
710            self.replacement(commit_id)
711                .is_some_and(|repl| repl.get_replacement_io().contains(&node))
712        };
713        let is_deleted = || self.deleted_nodes(commit_id).contains(&node);
714        self.contains_id(commit_id) && !is_replacement_io() && !is_deleted()
715    }
716
717    fn is_value_port(&self, PatchNode(commit_id, node): PatchNode, port: impl Into<Port>) -> bool {
718        self.commit_hugr(commit_id)
719            .get_optype(node)
720            .port_kind(port)
721            .expect("invalid port")
722            .is_value()
723    }
724}
725
726impl IntoIterator for PersistentHugr {
727    type Item = Commit;
728
729    type IntoIter = vec::IntoIter<Commit>;
730
731    fn into_iter(self) -> Self::IntoIter {
732        self.state_space
733            .all_commit_ids()
734            .map(|id| self.state_space.get_commit(id).clone())
735            .collect_vec()
736            .into_iter()
737    }
738}
739
740/// Find a node in `commit` that is invalidated by more than one child commit
741/// among `children`.
742fn find_conflicting_node<'a>(
743    commit_id: CommitId,
744    mut children: impl Iterator<Item = &'a Commit>,
745) -> Option<Node> {
746    let mut all_invalidated = BTreeSet::new();
747
748    children.find_map(|child| {
749        let mut new_invalidated =
750            child
751                .invalidation_set()
752                .filter_map(|PatchNode(del_commit_id, node)| {
753                    if del_commit_id == commit_id {
754                        Some(node)
755                    } else {
756                        None
757                    }
758                });
759        new_invalidated.find(|&n| !all_invalidated.insert(n))
760    })
761}
762
763#[cfg(test)]
764mod tests;