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;