diff --git a/Cargo.lock b/Cargo.lock index 8983dc3c1a..d93c233af2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2593,9 +2593,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "relrc" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65b9e2100a2ee7d9efb575064b2f9a552d7f9f60289d7b95b9c22175101c7c4a" +checksum = "f7c9d6253f1c4c3606b530b88ba15cd6803d4104ccd4d34173397ce49fe0b867" dependencies = [ "derive-where", "derive_more 0.99.20", @@ -2603,6 +2603,7 @@ dependencies = [ "itertools 0.13.0", "petgraph 0.8.3", "serde", + "slotmap_fork_lmondada", "thiserror 1.0.69", ] @@ -2924,6 +2925,16 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "slotmap_fork_lmondada" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936d3c60e7b845c8978d64485cffa451156294a772ebfafc0ea5fbd7a3c58669" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" diff --git a/Cargo.toml b/Cargo.toml index 5512aea60d..5c8cfee815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,7 +89,7 @@ pest_derive = "2.8.2" pretty = "0.12.5" pretty_assertions = "1.4.1" zstd = "0.13.2" -relrc = "0.4.6" +relrc = "0.5.0" wyhash = "0.6.0" # These public dependencies usually require breaking changes downstream, so we diff --git a/hugr-persistent/Cargo.toml b/hugr-persistent/Cargo.toml index 957c3dfe63..edce0a534e 100644 --- a/hugr-persistent/Cargo.toml +++ b/hugr-persistent/Cargo.toml @@ -39,4 +39,4 @@ bench = false rstest.workspace = true semver.workspace = true serde_with.workspace = true -insta.workspace = true +insta = { workspace = true, features = ["yaml"] } diff --git a/hugr-persistent/src/commit.rs b/hugr-persistent/src/commit.rs new file mode 100644 index 0000000000..ae6db1c825 --- /dev/null +++ b/hugr-persistent/src/commit.rs @@ -0,0 +1,439 @@ +//! [`Commit`] type and data associated with it. + +use std::{marker::PhantomData, mem}; + +use delegate::delegate; +use hugr_core::{ + Direction, Hugr, HugrView, IncomingPort, Node, OutgoingPort, Port, + hugr::{ + NodeMetadataMap, internal::HugrInternals, patch::simple_replace::InvalidReplacement, + views::InvalidSignature, + }, + ops::OpType, +}; +use itertools::{Either, Itertools}; +use relrc::RelRc; +use thiserror::Error; + +use crate::{ + CommitData, CommitId, CommitStateSpace, PatchNode, PersistentReplacement, + subgraph::InvalidPinnedSubgraph, +}; + +mod boundary; + +/// A single unit of change in a [`PersistentHugr`]. +/// +/// Invariant: there is always a unique root commit (i.e. a commit with variant +/// `CommitData::Base`) in the ancestors of a commit. +/// +/// The data within a commit is a patch, representing a rewrite that can be +/// performed on the Hugr defined by the ancestors of the commit. Currently, +/// patches must be [`SimpleReplacement`]s. +/// +/// # Lifetime of commits +/// +/// A commit remains valid as long as the [`CommitStateSpace`] containing it is +/// alive. Note that it is also sufficient that a [`PersistentHugr`] containing +/// the commit is alive, given that the [`CommitStateSpace`] is guaranteed to +/// be alive as long as any of its contained [`PersistentHugr`]s. In other +/// words, the lifetime dependency is: +/// ```ignore +/// PersistentHugr -> CommitStateSpace -> Commit +/// ``` +/// where `->` can be read as "is outlived by". Note that the dependencies are +/// NOT valid in the other direction: a [`Commit`] only maintains a weak +/// reference to its [`CommitStateSpace`]. +/// +/// When a [`CommitStateSpace`] goes out of scope, all its commits become +/// invalid. The implementation uses lifetimes to ensure at compile time that +/// the commit is valid throughout its lifetime. All constructors of [`Commit`] +/// thus expect a reference to the state space that the commit should be added +/// to, which fixes the lifetime of the commit. +/// +/// Methods that directly modify the lifetime are marked as `unsafe`. It is up +/// to the user to ensure that the commit is valid throughout its updated +/// lifetime. +/// +/// [`PersistentHugr`]: crate::PersistentHugr +/// [`SimpleReplacement`]: hugr_core::SimpleReplacement +#[derive(Debug, Clone)] +#[repr(transparent)] +pub struct Commit<'a>(RelRc, PhantomData<&'a ()>); + +impl<'a> Commit<'a> { + /// Create a commit from a simple replacement. + /// + /// Requires a reference to the commit state space that the commit should + /// be added to. + /// + /// Use [`Self::try_new`] instead if the parents of the commit cannot be + /// inferred from the invalidation set of `replacement` alone. + /// + /// The replacement must act on a non-empty subgraph, otherwise this + /// function will return an [`InvalidCommit::EmptyReplacement`] error. + /// + /// If any of the parents of the replacement are not in the commit state + /// space, this function will return an [`InvalidCommit::UnknownParent`] + /// error. + pub fn try_from_replacement( + replacement: PersistentReplacement, + state_space: &'a CommitStateSpace, + ) -> Result { + Self::try_new(replacement, [], state_space) + } + + /// Create a new base commit. + /// + /// Note that the base commit must be unique in the state space. This does + /// not check for uniqueness. + /// + /// Prefer using [`CommitStateSpace::try_set_base`] or + /// [`PersistentHugr::with_base`] to create base commits. + /// + /// [`PersistentHugr::with_base`]: crate::PersistentHugr::with_base + pub(crate) fn new_base(hugr: Hugr, state_space: &'a CommitStateSpace) -> Self { + let commit = RelRc::new(CommitData::Base(hugr)); + commit + .try_register_in(state_space.as_registry()) + .expect("new node is not yet registered"); + + Commit(commit, PhantomData) + } + + /// Create a new commit + /// + /// Requires a reference to the commit state space that the commit should + /// be added to. + /// + /// The returned commit will correspond to the application of `replacement` + /// and will be the child of the commits in `parents` as well as of all + /// the commits in the invalidation set of `replacement`. + /// + /// The replacement must act on a non-empty subgraph, otherwise this + /// function will return an [`InvalidCommit::EmptyReplacement`] error. + /// If any of the parents of the replacement are not in the commit state + /// space, this function will return an [`InvalidCommit::UnknownParent`] + /// error. + pub fn try_new<'b>( + replacement: PersistentReplacement, + parents: impl IntoIterator>, + state_space: &'a CommitStateSpace, + ) -> Result { + // TODO: clearly this needs to check that all state_space are the same?? + if replacement.subgraph().nodes().is_empty() { + return Err(InvalidCommit::EmptyReplacement); + } + let repl_parents = get_parent_commits(&replacement, state_space)? + .into_iter() + .map_into::>(); + let parents = parents + .into_iter() + .map_into::>() + .chain(repl_parents) + .unique_by(|p| p.as_ptr()); + let rc = RelRc::with_parents(replacement.into(), parents.into_iter().map(|p| (p, ()))); + + if let Err(err) = get_base_ancestors(&rc).exactly_one() { + return Err(InvalidCommit::NonUniqueBase(err.count())); + } + + rc.try_register_in(state_space.as_registry()) + .expect("new node is not yet registered"); + + Ok(Self(rc, PhantomData)) + } + + /// Create a commit from a relrc. + /// + /// This is unsafe because it cannot be guaranteed that the commit will + /// live as long as the lifetime 'a. + pub(crate) unsafe fn from_relrc(rc: RelRc) -> Self { + Self(rc, PhantomData) + } + + /// The state space that `self` belongs to. + pub fn state_space(&self) -> CommitStateSpace { + self.0 + .registry() + .expect("invalid commit: not registered") + .into() + } + + /// Get the ID of `self`. + pub fn id(&self) -> CommitId { + self.state_space() + .get_id(self) + .expect("invalid commit: not registered") + } + + /// Get the parents of `commit_id` + pub fn parents(&self) -> impl Iterator + '_ { + self.as_relrc() + .all_parents() + .map_into() + // SAFETY: the parents will be alive as long as self + .map(|cm: &Commit| unsafe { upgrade_lifetime(cm) }) + } + + /// Get all commits that have `self` as parent in `state_space`. + pub fn children(&self, _state_space: &'a CommitStateSpace) -> impl Iterator + '_ { + self.as_relrc() + .all_children() + // SAFETY: the children will be alive as long as the state space + // is alive + .map(|rc| unsafe { Self::from_relrc(rc) }) + } + + /// Check if `self` is a valid commit. + /// + /// This checks if + /// 1. there is exactly one ancestor of `self` that is a base commit, and + /// 2. `self` is registered in a state space. + pub fn is_valid(&self) -> bool { + get_base_ancestors(&self.0).exactly_one().is_ok() && self.0.registry().is_some() + } + + pub(crate) fn as_relrc(&self) -> &RelRc { + &self.0 + } + + /// Get the set of nodes inserted by `self`. + pub fn inserted_nodes(&self) -> impl Iterator + '_ { + match self.0.value() { + CommitData::Base(base) => Either::Left(base.nodes()), + CommitData::Replacement(repl) => { + // Skip the entrypoint and the IO nodes + Either::Right(repl.replacement().entry_descendants().skip(3)) + } + } + } + + /// Get the patch that `self` represents. + pub fn replacement(&self) -> Option<&PersistentReplacement> { + match self.0.value() { + CommitData::Base(_) => None, + CommitData::Replacement(replacement) => Some(replacement), + } + } + + /// Get the set of nodes in parent commits deleted by applying `self`. + /// + /// Currently this is the same as [`Self::invalidated_parent_nodes`]. + pub fn deleted_parent_nodes(&self) -> impl Iterator + '_ { + self.replacement() + .into_iter() + .flat_map(|r| r.invalidation_set()) + } + + /// Get the Hugr inserted by `self`. + /// + /// This is either the replacement Hugr of a [`CommitData::Replacement`] or + /// the base Hugr of a [`CommitData::Base`]. + pub(crate) fn commit_hugr(&self) -> &Hugr { + match self.value() { + CommitData::Base(base) => base, + CommitData::Replacement(repl) => repl.replacement(), + } + } + + delegate! { + to self.0 { + pub(crate) fn value(&self) -> &CommitData; + pub(crate) fn as_ptr(&self) -> *const relrc::node::InnerData; + } + } + + /// Get the base commit of `self`. + pub(crate) fn base_commit(&self) -> &Self { + let rc = get_base_ancestors(&self.0) + .next() + .expect("no base commit found"); + let commit: &Commit = rc.into(); + // SAFETY: base commit lives at least as long as `self` + unsafe { upgrade_lifetime(commit) } + } + + /// Check if `(node, port)` is a value port in `self`. + pub(crate) fn is_value_port(&self, node: Node, port: impl Into) -> bool { + self.commit_hugr() + .get_optype(node) + .port_kind(port) + .expect("invalid port") + .is_value() + } + + /// All value ports of `node` in `dir`. + pub(crate) fn value_ports( + &self, + node: Node, + dir: Direction, + ) -> impl Iterator + '_ { + let ports = self.node_ports(node, dir); + ports.filter_map(move |p| self.is_value_port(node, p).then_some((node, p))) + } + + /// All outgoing value ports of `node` in `self`. + pub(crate) fn output_value_ports( + &self, + node: Node, + ) -> impl Iterator + '_ { + self.value_ports(node, Direction::Outgoing) + .map(|(n, p)| (n, p.as_outgoing().expect("unexpected port direction"))) + } + + /// All incoming value ports of `node` in `self`. + pub(crate) fn input_value_ports( + &self, + node: Node, + ) -> impl Iterator + '_ { + self.value_ports(node, Direction::Incoming) + .map(|(n, p)| (n, p.as_incoming().expect("unexpected port direction"))) + } + + /// Change the lifetime of the commit. + /// + /// This is unsafe because it cannot be guaranteed that the commit will + /// live as long as the lifetime 'b. The user must guuarantee that the + /// rewrite space of `self` is valid as long as the lifetime 'b. + pub unsafe fn upgrade_lifetime<'b>(self) -> Commit<'b> { + Commit(self.0, PhantomData) + } +} + +/// Change the lifetime of the commit reference. +/// +/// This is unsafe because it cannot be guaranteed that the commit will +/// live as long as the lifetime 'b. +pub(crate) unsafe fn upgrade_lifetime<'a, 'b, 'c>(commit: &'c Commit<'a>) -> &'c Commit<'b> { + unsafe { mem::transmute(commit) } +} + +// The subset of HugrView methods that can be implemented on Commits +// by simplify delegating to the patches' respective HUGRs +impl Commit<'_> { + /// Get the type of the operation at `node`. + pub fn get_optype(&self, node: Node) -> &OpType { + let hugr = self.commit_hugr(); + hugr.get_optype(node) + } + + /// Get the number of ports of `node` in `dir`. + pub fn num_ports(&self, node: Node, dir: Direction) -> usize { + self.commit_hugr().num_ports(node, dir) + } + + /// Iterator over output ports of node. + /// Like [`CommitStateSpace::node_ports`](node, Direction::Outgoing)` + /// but preserves knowledge that the ports are [OutgoingPort]s. + #[inline] + pub fn node_outputs(&self, node: Node) -> impl Iterator + Clone + '_ { + self.node_ports(node, Direction::Outgoing) + .map(|p| p.as_outgoing().unwrap()) + } + + /// Iterator over inputs ports of node. + /// Like [`CommitStateSpace::node_ports`](node, Direction::Incoming)` + /// but preserves knowledge that the ports are [IncomingPort]s. + #[inline] + pub fn node_inputs(&self, node: Node) -> impl Iterator + Clone + '_ { + self.node_ports(node, Direction::Incoming) + .map(|p| p.as_incoming().unwrap()) + } + + /// Get an iterator over the ports of `node` in `dir`. + pub fn node_ports( + &self, + node: Node, + dir: Direction, + ) -> impl Iterator + Clone + '_ { + self.commit_hugr().node_ports(node, dir) + } + + /// Get an iterator over all ports of `node`. + pub fn all_node_ports(&self, node: Node) -> impl Iterator + Clone + '_ { + self.commit_hugr().all_node_ports(node) + } + + /// Get the metadata map of `node`. + pub fn node_metadata_map(&self, node: Node) -> &NodeMetadataMap { + self.commit_hugr().node_metadata_map(node) + } +} + +fn get_base_ancestors(arg: &RelRc) -> impl Iterator> { + arg.all_ancestors() + .filter(|c| matches!(c.value(), CommitData::Base(_))) +} + +impl From> for RelRc { + fn from(Commit(data, _): Commit) -> Self { + data + } +} + +impl<'a> From<&'a RelRc> for &'a Commit<'a> { + fn from(rc: &'a RelRc) -> Self { + // SAFETY: Commit is a transparent wrapper around RelRc + unsafe { mem::transmute(rc) } + } +} + +/// An error that occurs when trying to add a commit to a commit state space. +#[derive(Debug, Clone, Error)] +#[non_exhaustive] +pub enum InvalidCommit { + /// The commit conflicts with existing commits in the state space. + #[error("Incompatible history: children of commit {0:?} conflict in {1:?}")] + IncompatibleHistory(CommitId, Node), + + /// The commit has a parent not present in the state space. + #[error("Missing parent commit: {0:?}")] + UnknownParent(CommitId), + + /// The commit is not a replacement. + #[error("Commit is not a replacement")] + NotReplacement, + + /// The set of commits contains zero or more than one base commit. + #[error("{0} base commits found (should be 1)")] + NonUniqueBase(usize), + + /// The commit is an empty replacement. + #[error("Not allowed: empty replacement")] + EmptyReplacement, + + #[error("Invalid subgraph: {0}")] + /// The subgraph of the replacement is not convex. + InvalidSubgraph(#[from] InvalidPinnedSubgraph), + + /// The replacement of the commit is invalid. + #[error("Invalid replacement: {0}")] + InvalidReplacement(#[from] InvalidReplacement), + + /// The signature of the replacement is invalid. + #[error("Invalid signature: {0}")] + InvalidSignature(#[from] InvalidSignature), + + /// A wire has an unpinned port. + #[error("Incomplete wire: {0} is unpinned")] + IncompleteWire(PatchNode, Port), + + /// The commit ID is not in the state space. + #[error("Unknown commit ID: {0:?}")] + UnknownCommitId(CommitId), +} + +fn get_parent_commits<'a>( + replacement: &PersistentReplacement, + state_space: &'a CommitStateSpace, +) -> Result>, InvalidCommit> { + let parent_ids = replacement.invalidation_set().map(|n| n.owner()).unique(); + parent_ids + .map(|id| { + state_space + .try_upgrade(id) + .ok_or(InvalidCommit::UnknownParent(id)) + }) + .collect() +} diff --git a/hugr-persistent/src/commit/boundary.rs b/hugr-persistent/src/commit/boundary.rs new file mode 100644 index 0000000000..08c03c979f --- /dev/null +++ b/hugr-persistent/src/commit/boundary.rs @@ -0,0 +1,221 @@ +//! Methods for traversing wires at commit boundaries + +use std::collections::BTreeSet; + +use hugr_core::{ + HugrView, IncomingPort, Node, OutgoingPort, Port, + hugr::patch::{BoundaryPort, simple_replace::BoundaryMode}, +}; +use itertools::{Either, Itertools}; + +use crate::{Commit, PatchNode, parents_view::ParentsView}; + +impl Commit<'_> { + /// Translate a node in the commit's HUGR into a [`PatchNode`] used for + /// node indexing within [`PersistentHugr`]s. + /// + /// [`PersistentHugr`]: crate::PersistentHugr + pub fn to_patch_node(&self, node: Node) -> PatchNode { + PatchNode(self.id(), node) + } + + /// Whether the wire attached to `(node, port)` in `self` is a boundary edge + /// into `child`. + /// + /// In other words, check if `(node, port)` is outside of the subgraph of + /// the patch of `child` and at least one opposite node is inside the + /// subgraph. + fn has_boundary_edge_into(&self, node: Node, port: impl Into, child: &Commit) -> bool { + let deleted_nodes: BTreeSet<_> = child.deleted_parent_nodes().collect(); + if deleted_nodes.contains(&self.to_patch_node(node)) { + return false; + } + let mut opp_nodes = self + .commit_hugr() + .linked_ports(node, port) + .map(|(n, _)| PatchNode(self.id(), n)); + + opp_nodes.any(|n| deleted_nodes.contains(&n)) + } + + /// Get the boundary inputs in `child` linked to `(node, port)` in `self`. + /// + /// The returned ports will be ports on successors of the input node in the + /// `child` commit, unless (node, port) is connected to a passthrough wire + /// in `child` (i.e. a wire from input node to output node), in which + /// case they will be in one of the parents of `child`. + /// + /// `child` should be a child commit of the owner of `node`. + /// + /// ## Panics + /// + /// Panics if `(node, port)` is not a boundary edge, if `child` is not + /// a valid commit ID or if it is the base commit. + pub(crate) fn linked_child_inputs( + &self, + node: Node, + port: OutgoingPort, + child: &Commit, + return_invalid: BoundaryMode, + ) -> impl Iterator + '_ { + assert!( + self.has_boundary_edge_into(node, port, child), + "not a boundary edge" + ); + + let parent_hugrs = ParentsView::from_commit(child); + let repl = child.replacement().expect("valid child commit"); + let child_id = child.id(); + repl.linked_replacement_inputs( + (self.to_patch_node(node), port), + &parent_hugrs, + return_invalid, + ) + .collect_vec() + .into_iter() + .map(move |np| match np { + BoundaryPort::Host(patch_node, port) => (patch_node, port), + BoundaryPort::Replacement(node, port) => (PatchNode(child_id, node), port), + }) + } + + /// Get the single boundary output in `child` linked to `(node, port)` in + /// `self`. + /// + /// The returned port will be a port on a predecessor of the output node in + /// the `child` commit, unless (node, port) is connected to a passthrough + /// wire in `child` (i.e. a wire from input node to output node), in + /// which case it will be in one of the parents of `child`. + /// + /// `child` should be a child commit of the owner of `node` (or `None` will + /// be returned). + /// + /// ## Panics + /// + /// Panics if `child` is not a valid commit ID. + pub(crate) fn linked_child_output( + &self, + node: Node, + port: IncomingPort, + child: &Commit, + return_invalid: BoundaryMode, + ) -> Option<(PatchNode, OutgoingPort)> { + let parent_hugrs = ParentsView::from_commit(child); + let repl = child.replacement()?; + match repl.linked_replacement_output( + (self.to_patch_node(node), port), + &parent_hugrs, + return_invalid, + )? { + BoundaryPort::Host(patch_node, port) => (patch_node, port), + BoundaryPort::Replacement(node, port) => (child.to_patch_node(node), port), + } + .into() + } + + /// Get the boundary ports in `child` linked to `(node, port)` in `self`. + /// + /// `child` should be a child commit of the owner of `node`. + /// + /// See [`Self::linked_child_inputs`] and [`Self::linked_child_output`] for + /// more details. + pub(crate) fn linked_child_ports( + &self, + node: Node, + port: impl Into, + child: &Commit, + return_invalid: BoundaryMode, + ) -> impl Iterator + '_ { + match port.into().as_directed() { + Either::Left(incoming) => Either::Left( + self.linked_child_output(node, incoming, child, return_invalid) + .into_iter() + .map(|(node, port)| (node, port.into())), + ), + Either::Right(outgoing) => Either::Right( + self.linked_child_inputs(node, outgoing, child, return_invalid) + .map(|(node, port)| (node, port.into())), + ), + } + } + + /// Get the single output port linked to `(node, port)` in a parent of + /// `self`. + /// + /// The returned port belongs to the input boundary of the subgraph in + /// parent. + /// + /// ## Panics + /// + /// Panics if `(node, port)` is not connected to the input node in the + /// commit of `node`, or if the node is not valid. + pub fn linked_parent_input(&self, node: Node, port: IncomingPort) -> (PatchNode, OutgoingPort) { + let repl = self.replacement().expect("valid non-base commit"); + + assert!( + repl.replacement() + .input_neighbours(node) + .contains(&repl.get_replacement_io()[0]), + "not connected to input" + ); + + let parent_hugrs = ParentsView::from_commit(self); + repl.linked_host_input((node, port), &parent_hugrs).into() + } + + /// Get the input ports linked to `(node, port)` in a parent of `self`. + /// + /// The returned ports belong to the output boundary of the subgraph in + /// parent. + /// + /// ## Panics + /// + /// Panics if `(node, port)` is not connected to the output node in the + /// commit of `node`, or if the node is not valid. + pub fn linked_parent_outputs( + &self, + node: Node, + port: OutgoingPort, + ) -> impl Iterator + '_ { + let repl = self.replacement().expect("valid non-base commit"); + + assert!( + repl.replacement() + .output_neighbours(node) + .contains(&repl.get_replacement_io()[1]), + "not connected to output" + ); + + let parent_hugrs = ParentsView::from_commit(self); + repl.linked_host_outputs((node, port), &parent_hugrs) + .map_into() + .collect_vec() + .into_iter() + } + + /// Get the ports linked to `(node, port)` in a parent of `self`. + /// + /// See [`Self::linked_parent_input`] and [`Self::linked_parent_outputs`] + /// for more details. + /// + /// ## Panics + /// + /// Panics if `(node, port)` is not connected to an IO node in the commit + /// of `node`, or if the node is not valid. + pub fn linked_parent_ports( + &self, + node: Node, + port: impl Into, + ) -> impl Iterator + '_ { + match port.into().as_directed() { + Either::Left(incoming) => { + let (node, port) = self.linked_parent_input(node, incoming); + Either::Left(std::iter::once((node, port.into()))) + } + Either::Right(outgoing) => Either::Right( + self.linked_parent_outputs(node, outgoing) + .map(|(node, port)| (node, port.into())), + ), + } + } +} diff --git a/hugr-persistent/src/lib.rs b/hugr-persistent/src/lib.rs index 694e83e23e..b7391a8936 100644 --- a/hugr-persistent/src/lib.rs +++ b/hugr-persistent/src/lib.rs @@ -3,40 +3,50 @@ //! Persistent data structure for HUGR mutations. //! //! This crate provides a persistent data structure [`PersistentHugr`] that -//! implements [`HugrView`](hugr_core::HugrView); mutations to the data are -//! stored persistently as a set of [`Commit`]s along with the dependencies -//! between the commits. +//! implements [`HugrView`](hugr_core::HugrView). A persistent data structure: +//! - is cheap to clone, +//! - shares as much of the underlying data with other versions of the data +//! structure as possible, even as it gets mutated (unlike, say the Rust +//! `Cow` type), +//! - in our case, multiple versions of the same data structure can even be +//! merged together (with a notion of "compatibility" between versions). +//! +//! Persistent data structures are sometimes called immutable data structures, +//! as mutations can always be implemented as cheap clone + cheap mutation. +//! Mutations to the data are stored persistently as a set of [`Commit`]s along +//! with the dependencies between the commits. //! //! As a result of persistency, the entire mutation history of a HUGR can be //! traversed and references to previous versions of the data remain valid even //! as the HUGR graph is "mutated" by applying patches: the patches are in //! effect added to the history as new commits. //! -//! The data structure underlying [`PersistentHugr`], which stores the history -//! of all commits, is [`CommitStateSpace`]. Multiple [`PersistentHugr`] can be -//! stored within a single [`CommitStateSpace`], which allows for the efficient -//! exploration of the space of all possible graph rewrites. +//! Multiple [`PersistentHugr`] may use commits from a common +//! [`CommitStateSpace`]: [`PersistentHugr`]s derived from mutations of previous +//! [`PersistentHugr`]s will always share a common state space. A [`Walker`] +//! can be used on a state space to explore all possible [`PersistentHugr`]s +//! that can be created from commits in the space. //! //! ## Overlapping commits //! //! In general, [`CommitStateSpace`] may contain overlapping commits. Such //! mutations are mutually exclusive as they modify the same nodes. It is //! therefore not possible to apply all commits in a [`CommitStateSpace`] -//! simultaneously. A [`PersistentHugr`] on the other hand always corresponds to -//! a subgraph of a [`CommitStateSpace`] that is guaranteed to contain only -//! non-overlapping, compatible commits. By applying all commits in a -//! [`PersistentHugr`], we can materialize a [`Hugr`](hugr_core::Hugr). -//! Traversing the materialized HUGR is equivalent to using the -//! [`HugrView`](hugr_core::HugrView) implementation of the corresponding -//! [`PersistentHugr`]. +//! simultaneously. A [`PersistentHugr`] on the other hand always contains a +//! a subset of the commits of a [`CommitStateSpace`], with the guarantee +//! that they are all non-overlapping, compatible commits. By applying all +//! commits in a [`PersistentHugr`], we can materialize a +//! [`Hugr`](hugr_core::Hugr). Traversing the materialized HUGR is equivalent to +//! using the [`HugrView`](hugr_core::HugrView) implementation of the +//! corresponding [`PersistentHugr`]. //! //! ## Summary of data types //! //! - [`Commit`] A modification to a [`Hugr`](hugr_core::Hugr) (currently a //! [`SimpleReplacement`](hugr_core::SimpleReplacement)) that forms the atomic //! unit of change for a [`PersistentHugr`] (like a commit in git). This is a -//! reference-counted value that is cheap to clone and will be freed when the -//! last reference is dropped. +//! reference-counted value that is cheap to clone and will be freed when all +//! [`PersistentHugr`] and [`CommitStateSpace`] that refer to it are dropped. //! - [`PersistentHugr`] A data structure that implements //! [`HugrView`][hugr_core::HugrView] and can be used as a drop-in replacement //! for a [`Hugr`][hugr_core::Hugr] for read-only access and mutations through @@ -44,10 +54,10 @@ //! [`Patch`](hugr_core::hugr::Patch) traits. Mutations are stored as a //! history of commits. Unlike [`CommitStateSpace`], it maintains the //! invariant that all contained commits are compatible with eachother. -//! - [`CommitStateSpace`] Stores commits, recording the dependencies between -//! them. Includes the base HUGR and any number of possibly incompatible -//! (overlapping) commits. Unlike a [`PersistentHugr`], a state space can -//! contain mutually exclusive commits. +//! - [`CommitStateSpace`] A registry of all commits within the +//! [`PersistentHugr`]s of the state space. Includes the base HUGR and any +//! number of possibly incompatible (overlapping) commits. Unlike a +//! [`PersistentHugr`], a state space can contain mutually exclusive commits. //! //! ## Usage //! @@ -57,37 +67,37 @@ //! have a populated state space, use [`PersistentHugr::try_new`] to create a //! new HUGR with those commits. //! -//! Add a sequence of commits to a state space by merging a [`PersistentHugr`] -//! into it using [`CommitStateSpace::extend`] or directly using -//! [`CommitStateSpace::try_add_commit`]. -//! //! To obtain a [`PersistentHugr`] from your state space, use -//! [`CommitStateSpace::try_extract_hugr`]. A [`PersistentHugr`] can always be +//! [`CommitStateSpace::try_create`]. A [`PersistentHugr`] can always be //! materialized into a [`Hugr`][hugr_core::Hugr] type using //! [`PersistentHugr::to_hugr`]. +pub mod commit; +pub use commit::{Commit, InvalidCommit}; + mod parents_view; -mod persistent_hugr; -mod resolver; + +pub mod persistent_hugr; +pub use persistent_hugr::PersistentHugr; + pub mod state_space; +use state_space::CommitData; +pub use state_space::{CommitId, CommitStateSpace, PatchNode}; + pub mod subgraph; +pub use subgraph::PinnedSubgraph; + mod trait_impls; -pub mod walker; -mod wire; -pub use persistent_hugr::{Commit, PersistentHugr}; -pub use resolver::{PointerEqResolver, Resolver, SerdeHashResolver}; -pub use state_space::{CommitId, CommitStateSpace, InvalidCommit, PatchNode}; -pub use subgraph::PinnedSubgraph; +pub mod walker; pub use walker::Walker; + +mod wire; pub use wire::PersistentWire; /// A replacement operation that can be applied to a [`PersistentHugr`]. pub type PersistentReplacement = hugr_core::SimpleReplacement; -use persistent_hugr::find_conflicting_node; -use state_space::CommitData; - pub mod serial { //! Serialized formats for commits, state spaces and persistent HUGRs. pub use super::persistent_hugr::serial::*; diff --git a/hugr-persistent/src/parents_view.rs b/hugr-persistent/src/parents_view.rs index 536023109a..53ce418c6b 100644 --- a/hugr-persistent/src/parents_view.rs +++ b/hugr-persistent/src/parents_view.rs @@ -11,7 +11,9 @@ use hugr_core::{ ops::OpType, }; -use super::{CommitStateSpace, PatchNode, state_space::CommitId}; +use crate::Commit; + +use super::{PatchNode, state_space::CommitId}; /// A HugrView on the (disjoint) union of all parent HUGRs of a commit. /// @@ -23,13 +25,10 @@ pub(crate) struct ParentsView<'a> { } impl<'a> ParentsView<'a> { - pub(crate) fn from_commit( - commit_id: CommitId, - state_space: &'a CommitStateSpace, - ) -> Self { + pub(crate) fn from_commit(commit: &'a Commit) -> Self { let mut hugrs = BTreeMap::new(); - for parent in state_space.parents(commit_id) { - hugrs.insert(parent, state_space.commit_hugr(parent)); + for parent in commit.parents() { + hugrs.insert(parent.id(), parent.commit_hugr()); } Self { hugrs } } diff --git a/hugr-persistent/src/persistent_hugr.rs b/hugr-persistent/src/persistent_hugr.rs index bdca32ec1f..4635343f4a 100644 --- a/hugr-persistent/src/persistent_hugr.rs +++ b/hugr-persistent/src/persistent_hugr.rs @@ -1,186 +1,27 @@ use std::{ - collections::{BTreeSet, HashMap}, - mem, vec, + collections::{BTreeSet, HashMap, VecDeque}, + vec, }; -use delegate::delegate; -use derive_more::derive::From; use hugr_core::{ - Direction, Hugr, HugrView, IncomingPort, Node, OutgoingPort, Port, SimpleReplacement, + Hugr, HugrView, Node, hugr::patch::{Patch, simple_replace}, }; -use itertools::{Either, Itertools}; -use relrc::RelRc; +use itertools::Itertools; +use relrc::HistoryGraph; use crate::{ - CommitData, CommitId, CommitStateSpace, InvalidCommit, PatchNode, PersistentReplacement, - Resolver, + Commit, CommitData, CommitId, CommitStateSpace, InvalidCommit, PatchNode, PersistentReplacement, }; pub mod serial; -/// A patch that can be applied to a [`PersistentHugr`] or a -/// [`CommitStateSpace`] as an atomic commit. -/// -/// Commits are cheap to clone: they are reference-counted pointers to the -/// patch data. They also maintain strong references to the ancestor commits -/// that the patch may depend on (i.e. other patches that must be applied -/// before `self` can be applied). -/// -/// Currently, patches must be [`SimpleReplacement`]s. -#[derive(Debug, Clone, From)] -#[repr(transparent)] -pub struct Commit(RelRc); - -impl Commit { - /// Create a commit from a simple replacement. - /// - /// Requires a reference to the commit state space that the nodes in - /// `replacement` refer to. - /// - /// Use [`Self::try_new`] instead if the parents of the commit cannot be - /// inferred from the invalidation set of `replacement` alone. - /// - /// The replacement must act on a non-empty subgraph, otherwise this - /// function will return an [`InvalidCommit::EmptyReplacement`] error. - /// - /// If any of the parents of the replacement are not in the commit state - /// space, this function will return an [`InvalidCommit::UnknownParent`] - /// error. - pub fn try_from_replacement( - replacement: PersistentReplacement, - graph: &CommitStateSpace, - ) -> Result { - Self::try_new(replacement, [], graph) - } - - /// Create a new commit - /// - /// Requires a reference to the commit state space that the nodes in - /// `replacement` refer to. - /// - /// The returned commit will correspond to the application of `replacement` - /// and will be the child of the commits in `parents` as well as of all - /// the commits in the invalidation set of `replacement`. - /// - /// The replacement must act on a non-empty subgraph, otherwise this - /// function will return an [`InvalidCommit::EmptyReplacement`] error. - /// If any of the parents of the replacement are not in the commit state - /// space, this function will return an [`InvalidCommit::UnknownParent`] - /// error. - pub fn try_new( - replacement: PersistentReplacement, - parents: impl IntoIterator, - graph: &CommitStateSpace, - ) -> Result { - if replacement.subgraph().nodes().is_empty() { - return Err(InvalidCommit::EmptyReplacement); - } - let repl_parents = get_parent_commits(&replacement, graph)?; - let parents = parents - .into_iter() - .chain(repl_parents) - .unique_by(|p| p.as_ptr()); - let rc = RelRc::with_parents( - replacement.into(), - parents.into_iter().map(|p| (p.into(), ())), - ); - Ok(Self(rc)) - } - - pub(crate) fn as_relrc(&self) -> &RelRc { - &self.0 - } - - /// Get the set of nodes inserted by the patch in `self`. - pub fn inserted_nodes(&self) -> impl Iterator + '_ { - match self.0.value() { - CommitData::Base(base) => Either::Left(base.nodes()), - CommitData::Replacement(repl) => { - // Skip the entrypoint and the IO nodes - Either::Right(repl.replacement().entry_descendants().skip(3)) - } - } - } - - fn all_parents(&self) -> impl Iterator + '_ { - self.0.all_parents().map_into() - } - - /// Get the patch that `self` represents. - pub fn replacement(&self) -> Option<&PersistentReplacement> { - match self.0.value() { - CommitData::Base(_) => None, - CommitData::Replacement(replacement) => Some(replacement), - } - } - - /// Get the set of nodes invalidated by the patch in `self`. - pub fn invalidation_set(&self) -> impl Iterator + '_ { - self.replacement() - .into_iter() - .flat_map(|r| r.invalidation_set()) - } - - /// Get the set of nodes deleted by applying `self`. - pub fn deleted_nodes(&self) -> impl Iterator + '_ { - self.replacement() - .into_iter() - .flat_map(|r| r.subgraph().nodes()) - .copied() - } - - delegate! { - to self.0 { - pub(crate) fn value(&self) -> &CommitData; - pub(crate) fn as_ptr(&self) -> *const relrc::node::InnerData; - } - } - - /// Get all ancestors of `self` in reverse topological order, up until and - /// including the first commit for which `continue_fn` returns false. - fn get_ancestors_while<'a>( - &'a self, - continue_fn: impl Fn(&'a Commit) -> bool, - ) -> Vec<&'a Commit> { - let mut next_ancestor = 0; - let mut ancestors = vec![self]; - let mut seen = BTreeSet::from_iter([self.as_ptr()]); - while next_ancestor < ancestors.len() { - let commit = &ancestors[next_ancestor]; - next_ancestor += 1; - if !continue_fn(commit) { - continue; - } - for parent in commit.all_parents() { - if seen.insert(parent.as_ptr()) { - ancestors.push(parent); - } - } - } - ancestors - } -} - -impl From for RelRc { - fn from(Commit(data): Commit) -> Self { - data - } -} - -impl<'a> From<&'a RelRc> for &'a Commit { - fn from(rc: &'a RelRc) -> Self { - // SAFETY: Commit is a transparent wrapper around RelRc - unsafe { mem::transmute(rc) } - } -} - /// A HUGR-like object that tracks its mutation history. /// /// When mutations are applied to a [`PersistentHugr`], the object is mutated /// as expected but all references to previous versions of the object remain /// valid. Furthermore, older versions of the data can be recovered by -/// traversing the object's history with [`Self::as_state_space`]. +/// traversing the object's history with [`Self::state_space`]. /// /// Multiple references to various versions of a Hugr can be maintained in /// parallel by extracting them from a shared [`CommitStateSpace`]. @@ -211,32 +52,48 @@ impl<'a> From<&'a RelRc> for &'a Commit { /// /// ## Supported graph types /// -/// Currently, only patches that apply to subgraphs within dataflow regions -/// are supported. +/// Currently, only patches represented as [`SimpleReplacement`],which apply to +/// subgraphs within dataflow regions are supported. +/// +/// [`SimpleReplacement`]: hugr_core::SimpleReplacement #[derive(Clone, Debug)] -pub struct PersistentHugr { +pub struct PersistentHugr { /// The state space of all commits. /// - /// Invariant: all commits are "compatible", meaning that no two patches - /// invalidate the same node. - state_space: CommitStateSpace, + /// Invariants: + /// - all commits are "compatible", meaning that no two patches invalidate + /// the same node. + /// - there is a unique commit of variant [`CommitData::Base`] and its ID + /// is `base_commit_id`. + graph: HistoryGraph, + /// Cache of the unique root of the commit graph. + /// + /// The only commit in the graph with variant [`CommitData::Base`]. All + /// other commits are [`CommitData::Replacement`]s, and are descendants + /// of this. + /// + /// Invariant: any path from any commit in `self` through ancestors will + /// always lead to this commit. + base_commit_id: CommitId, } -impl PersistentHugr { +impl PersistentHugr { /// Create a [`PersistentHugr`] with `hugr` as its base HUGR. /// /// All replacements added in the future will apply on top of `hugr`. pub fn with_base(hugr: Hugr) -> Self { - let state_space = CommitStateSpace::with_base(hugr); - Self { state_space } + let state_space = CommitStateSpace::new(); + let base = state_space.try_set_base(hugr).expect("empty state space"); + Self::from_commit(base) } /// Create a [`PersistentHugr`] from a single commit and its ancestors. + /// + /// Panics if the commit is invalid. // This always defines a valid `PersistentHugr` as the ancestors of a commit // are guaranteed to be compatible with each other. pub fn from_commit(commit: Commit) -> Self { - let state_space = CommitStateSpace::try_from_commits([commit]).expect("commit is valid"); - Self { state_space } + Self::try_new([commit]).expect("invalid commit") } /// Create a [`PersistentHugr`] from a list of commits. @@ -247,9 +104,49 @@ impl PersistentHugr { /// If the state space of the commits would include two commits which are /// incompatible, or if the commits do not share a common base HUGR, then /// an error is returned. - pub fn try_new(commits: impl IntoIterator) -> Result { - let graph = CommitStateSpace::try_from_commits(commits)?; - graph.try_extract_hugr(graph.all_commit_ids()) + pub fn try_new<'a>( + commits: impl IntoIterator>, + ) -> Result { + let commits = get_ancestors_while(commits, |_| true); + let state_space = commits + .front() + .ok_or(InvalidCommit::NonUniqueBase(0))? + .state_space(); + let all_commit_ids = BTreeSet::from_iter(commits.iter().map(|c| c.as_ptr())); + let mut graph = HistoryGraph::with_registry(state_space.to_registry()); + + for commit in commits { + // Check that all commits are in the same state space + if commit.state_space() != state_space { + return Err(InvalidCommit::NonUniqueBase(2)); + } + + // Check that all commits are compatible + let selected_children = commit + .children(&state_space) + .filter(|c| all_commit_ids.contains(&c.as_ptr())); + if let Some(node) = find_conflicting_node(commit.id(), selected_children) { + return Err(InvalidCommit::IncompatibleHistory(commit.id(), node)); + } + + graph.insert_node(commit.into()); + } + + let base_commit = graph + .all_node_ids() + .filter(|&id| { + matches!( + graph.get_node(id).expect("valid ID").value(), + CommitData::Base(_) + ) + }) + .exactly_one() + .map_err(|err| InvalidCommit::NonUniqueBase(err.count()))?; + + Ok(Self { + graph, + base_commit_id: base_commit, + }) } /// Add a replacement to `self`. @@ -290,13 +187,18 @@ impl PersistentHugr { .into_grouping_map() .collect::>(); for (parent, new_invalid_nodes) in new_invalid_nodes { - let invalidation_set = self.invalidation_set(parent).collect(); + let invalidation_set = self.deleted_nodes(parent).collect(); if let Some(&node) = new_invalid_nodes.intersection(&invalidation_set).next() { return Err(InvalidCommit::IncompatibleHistory(parent, node)); } } - self.state_space.try_add_replacement(replacement) + let commit = Commit::try_from_replacement(replacement, self.state_space())?; + // SAFETY: commit does not need to be restrained to state_space's lifetime + // as it will be added to self. + let commit = unsafe { commit.upgrade_lifetime() }; + + self.try_add_commit(commit) } /// Add a commit to `self` and all its ancestors. @@ -306,39 +208,136 @@ impl PersistentHugr { /// [`InvalidCommit::IncompatibleHistory`] error is returned. In this case, /// as many compatible commits as possible are added to `self`. pub fn try_add_commit(&mut self, commit: Commit) -> Result { - let new_commits = commit.get_ancestors_while(|c| !self.contains(c)); - let mut commit_id = None; - for &commit in new_commits.iter().rev() { - commit_id = Some(self.state_space.try_add_commit(commit.clone())?); - let commit_id = commit_id.unwrap(); + self.try_add_commits([commit.clone()])?; + Ok(commit.id()) + } + + /// Add commits and their ancestors to `self`. + /// + /// The commits and all their ancestors must be compatible with all existing + /// commits in `self`. If this is not satisfied, an + /// [`InvalidCommit::IncompatibleHistory`] error is returned. In this case, + /// as many compatible commits as possible are added to `self`. + pub fn try_add_commits<'a>( + &mut self, + commits: impl IntoIterator>, + ) -> Result<(), InvalidCommit> { + let new_commits = get_ancestors_while(commits, |c| !self.contains(c)); + + for new_commit in new_commits.iter().rev() { + let new_commit_id = new_commit.id(); + if &new_commit.state_space() != self.state_space() { + return Err(InvalidCommit::NonUniqueBase(2)); + } // Check that the new commit is compatible with all its (current and // future) children let curr_children = self - .state_space - .children(commit_id) + .children_commits(new_commit_id) .map(|id| self.get_commit(id)); let new_children = new_commits .iter() - .copied() - .filter(|c| c.all_parents().any(|p| p.as_ptr() == commit.as_ptr())); + .filter(|&c| c.parents().any(|p| p.as_ptr() == new_commit.as_ptr())); if let Some(node) = find_conflicting_node( - commit_id, - curr_children.chain(new_children).unique_by(|c| c.as_ptr()), + new_commit_id, + curr_children + .chain(new_children) + .unique_by(|c| c.as_ptr()) + .map(|c| c.to_owned()), ) { - return Err(InvalidCommit::IncompatibleHistory(commit_id, node)); + return Err(InvalidCommit::IncompatibleHistory(new_commit_id, node)); } + + self.graph.insert_node(new_commit.clone().into()); } - Ok(commit_id.expect("new_commits cannot be empty")) + + Ok(()) } -} -impl PersistentHugr { - /// Construct a [`PersistentHugr`] from a [`CommitStateSpace`]. + /// Check the [`PersistentHugr`] invariants. + pub fn is_valid(&self) -> Result<(), InvalidCommit> { + let mut found_base = false; + for id in self.all_commit_ids() { + let commit = self.get_commit(id); + if matches!(commit.value(), CommitData::Base(_)) { + found_base = true; + if id != self.base_commit_id { + return Err(InvalidCommit::NonUniqueBase(2)); + } + } + let children = self + .children_commits(id) + .map(|child_id| self.get_commit(child_id).clone()); + if let Some(already_invalid) = find_conflicting_node(id, children) { + return Err(InvalidCommit::IncompatibleHistory(id, already_invalid)); + } + } + + if !found_base { + return Err(InvalidCommit::NonUniqueBase(0)); + } + + Ok(()) + } + + /// Get a reference to the underlying state space of `self`. + pub fn state_space(&self) -> &CommitStateSpace { + self.graph.registry().into() + } + + /// Get the base commit ID. + pub fn base(&self) -> CommitId { + self.base_commit_id + } + + /// Get the base [`Hugr`]. + pub fn base_hugr(&self) -> &Hugr { + let CommitData::Base(hugr) = self.get_commit(self.base_commit_id).value() else { + panic!("base commit is not a base hugr"); + }; + hugr + } + + /// Get the commit with ID `commit_id`. /// - /// Does not check that the commits are compatible. - pub(crate) fn from_state_space_unsafe(state_space: CommitStateSpace) -> Self { - Self { state_space } + /// Panics if `commit_id` is not in `self`. + pub fn get_commit(&self, commit_id: CommitId) -> &Commit<'_> { + self.graph + .get_node(commit_id) + .expect("invalid commit ID") + .into() + } + /// Check if `commit` is in the PersistentHugr. + pub fn contains(&self, commit: &Commit) -> bool { + self.graph.contains(commit.as_relrc()) + } + + /// Check if `commit_id` is in the PersistentHugr. + pub fn contains_id(&self, commit_id: CommitId) -> bool { + self.graph.contains_id(commit_id) + } + + /// Get the base commit. + pub fn base_commit(&self) -> &Commit<'_> { + self.get_commit(self.base()) + } + + /// Get an iterator over all commit IDs in the persistent HUGR. + pub fn all_commit_ids(&self) -> impl Iterator + Clone + '_ { + self.graph.all_node_ids() + } + + /// Get all commits in `self` in topological order. + fn toposort_commits(&self) -> Vec { + petgraph::algo::toposort(&self.graph, None).expect("history is a DAG") + } + + pub fn children_commits(&self, commit_id: CommitId) -> impl Iterator + '_ { + self.graph.children(commit_id) + } + + pub fn parent_commits(&self, commit_id: CommitId) -> impl Iterator + '_ { + self.graph.parents(commit_id) } /// Convert this `PersistentHugr` to a materialized Hugr by applying all @@ -356,13 +355,10 @@ impl PersistentHugr { /// Also returns a map from the nodes of the base HUGR to the nodes of the /// materialized HUGR. pub fn apply_all(&self) -> (Hugr, HashMap) { - let mut hugr = self.state_space.base_hugr().clone(); - let mut node_map = HashMap::from_iter( - hugr.nodes() - .map(|n| (PatchNode(self.state_space.base(), n), n)), - ); + let mut hugr = self.base_hugr().clone(); + let mut node_map = HashMap::from_iter(hugr.nodes().map(|n| (PatchNode(self.base(), n), n))); for commit_id in self.toposort_commits() { - let Some(repl) = self.state_space.get_commit(commit_id).replacement() else { + let Some(repl) = self.get_commit(commit_id).replacement() else { continue; }; @@ -398,86 +394,6 @@ impl PersistentHugr { (hugr, node_map) } - /// Get a reference to the underlying state space of `self`. - pub fn as_state_space(&self) -> &CommitStateSpace { - &self.state_space - } - - /// Convert `self` into its underlying [`CommitStateSpace`]. - pub fn into_state_space(self) -> CommitStateSpace { - self.state_space - } - - /// The unique outgoing port in `self` that `port` is attached to. - /// - /// # Panics - /// - /// Panics if `node` is not in `self` (in particular if it is deleted) or if - /// `port` is not a value port in `node`. - pub(crate) fn single_outgoing_port( - &self, - node: PatchNode, - port: impl Into, - ) -> (PatchNode, OutgoingPort) { - let w = self.get_wire(node, port.into()); - w.single_outgoing_port(self) - .expect("found invalid dfg wire") - } - - /// All incoming ports that the given outgoing port is attached to. - /// - /// # Panics - /// - /// Panics if `out_node` is not in `self` (in particular if it is deleted) - /// or if `out_port` is not a value port in `out_node`. - pub(crate) fn all_incoming_ports( - &self, - out_node: PatchNode, - out_port: OutgoingPort, - ) -> impl Iterator { - let w = self.get_wire(out_node, out_port); - w.into_all_ports(self, Direction::Incoming) - .map(|(node, port)| (node, port.as_incoming().unwrap())) - } - - delegate! { - to self.state_space { - /// Check if `commit` is in the PersistentHugr. - pub fn contains(&self, commit: &Commit) -> bool; - /// Check if `commit_id` is in the PersistentHugr. - pub fn contains_id(&self, commit_id: CommitId) -> bool; - /// Get the base commit ID. - pub fn base(&self) -> CommitId; - /// Get the base [`Hugr`]. - pub fn base_hugr(&self) -> &Hugr; - /// Get the base commit. - pub fn base_commit(&self) -> &Commit; - /// Get the commit with ID `commit_id`. - pub fn get_commit(&self, commit_id: CommitId) -> &Commit; - /// Check whether `commit_id` exists and return it. - pub fn try_get_commit(&self, commit_id: CommitId) -> Option<&Commit>; - /// Get an iterator over all nodes inserted by `commit_id`. - /// - /// All nodes will be PatchNodes with commit ID `commit_id`. - pub fn inserted_nodes(&self, commit_id: CommitId) -> impl Iterator + '_; - /// Get the replacement for `commit_id`. - pub(crate) fn replacement(&self, commit_id: CommitId) -> Option<&SimpleReplacement>; - /// Get the Hugr inserted by `commit_id`. - /// - /// This is either the replacement Hugr of a [`CommitData::Replacement`] or - /// the base Hugr of a [`CommitData::Base`]. - pub(crate) fn commit_hugr(&self, commit_id: CommitId) -> &Hugr; - /// Get an iterator over all commit IDs in the persistent HUGR. - pub fn all_commit_ids(&self) -> impl Iterator + Clone + '_; - } - } - - /// Get all commits in `self` in topological order. - fn toposort_commits(&self) -> Vec { - petgraph::algo::toposort(self.state_space.as_history_graph(), None) - .expect("history is a DAG") - } - /// Get the set of nodes of `commit_id` that are invalidated by the patches /// in the children commits of `commit_id`. /// @@ -485,109 +401,98 @@ impl PersistentHugr { /// children commits (as returned by [`Self::deleted_nodes`]), but may /// also include further nodes to enforce stricter exclusivity constraints /// between patches. - pub fn invalidation_set(&self, commit_id: CommitId) -> impl Iterator + '_ { - let children = self.state_space.children(commit_id); - children - .flat_map(move |child_id| self.state_space.invalidation_set(child_id, commit_id)) - .unique() - } - - /// Get the set of nodes of `commit_id` that are deleted by applying - /// the children commits of `commit_id`. - /// - /// This is a subset of [`Self::invalidation_set`]. Whilst the latter is - /// used to establish exclusivity constraints between patches, this method - /// is used when we are computing the set of nodes currently present in - /// `self`. - pub fn deleted_nodes(&self, commit_id: CommitId) -> impl Iterator + '_ { - let children = self.state_space.children(commit_id); - children - .flat_map(move |child_id| { - let child = self.get_commit(child_id); - child - .deleted_nodes() - .filter_map(move |PatchNode(id, node)| (commit_id == id).then_some(node)) - }) - .unique() - } - - /// Get the child commit that deletes `node`. - pub(crate) fn find_deleting_commit( - &self, - node @ PatchNode(commit_id, _): PatchNode, - ) -> Option { - let mut children = self.state_space.children(commit_id); - children.find(move |&child_id| { - let child = self.get_commit(child_id); - child.deleted_nodes().contains(&node) + pub fn deleted_nodes<'a>(&'a self, commit_id: CommitId) -> impl Iterator + 'a { + self.children_commits(commit_id).flat_map(move |child_id| { + let all_invalidated = self.get_commit(child_id).deleted_parent_nodes(); + all_invalidated + .filter_map(move |PatchNode(owner, node)| (owner == commit_id).then_some(node)) }) } - /// Convert a node ID specific to a commit HUGR into a patch node in the - /// [`PersistentHugr`]. - pub(crate) fn to_persistent_node(&self, node: Node, commit_id: CommitId) -> PatchNode { - PatchNode(commit_id, node) - } - /// Check if a patch node is in the PersistentHugr, that is, it belongs to /// a commit in the state space and is not deleted by any child commit. pub fn contains_node(&self, PatchNode(commit_id, node): PatchNode) -> bool { let is_replacement_io = || { - self.replacement(commit_id) + let commit = self.get_commit(commit_id); + commit + .replacement() .is_some_and(|repl| repl.get_replacement_io().contains(&node)) }; let is_deleted = || self.deleted_nodes(commit_id).contains(&node); self.contains_id(commit_id) && !is_replacement_io() && !is_deleted() } +} - pub(crate) fn is_value_port( - &self, - PatchNode(commit_id, node): PatchNode, - port: impl Into, - ) -> bool { - self.commit_hugr(commit_id) - .get_optype(node) - .port_kind(port) - .expect("invalid port") - .is_value() - } +/// Get the union of commits with all its ancestors, up to and including the +/// first commits for which `continue_fn` returns false. +/// +/// Return all ancestors in reverse topological order. +fn get_ancestors_while<'a>( + commits: impl IntoIterator>, + continue_fn: impl Fn(&Commit) -> bool, +) -> VecDeque> { + let mut seen_ids = BTreeSet::new(); + let commits = commits.into_iter(); + let mut all_commits = VecDeque::with_capacity(commits.size_hint().0); + + for commit in commits { + if !seen_ids.insert(commit.as_ptr()) { + continue; + } + let start = all_commits.len(); + let mut ind = start; + all_commits.push_back(commit); - pub(super) fn value_ports( - &self, - patch_node @ PatchNode(commit_id, node): PatchNode, - dir: Direction, - ) -> impl Iterator + '_ { - let hugr = self.commit_hugr(commit_id); - let ports = hugr.node_ports(node, dir); - ports.filter_map(move |p| self.is_value_port(patch_node, p).then_some((patch_node, p))) + while ind < all_commits.len() { + let commit = all_commits[ind].clone(); + ind += 1; + + if !continue_fn(&commit) { + continue; + } + + for commit in commit.parents() { + if seen_ids.insert(commit.as_ptr()) { + all_commits.push_back(commit.clone()); + } + } + } + all_commits.rotate_right(all_commits.len() - start); } - pub(super) fn output_value_ports( - &self, - patch_node: PatchNode, - ) -> impl Iterator + '_ { - self.value_ports(patch_node, Direction::Outgoing) - .map(|(n, p)| (n, p.as_outgoing().expect("unexpected port direction"))) + all_commits +} + +// non-public methods +impl PersistentHugr { + /// Convert a node ID specific to a commit HUGR into a patch node in the + /// [`PersistentHugr`]. + pub(crate) fn to_persistent_node(&self, node: Node, commit_id: CommitId) -> PatchNode { + PatchNode(commit_id, node) } - pub(super) fn input_value_ports( + /// Get the unique child commit in `self` that deletes `node`. + pub(crate) fn find_deleting_commit( &self, - patch_node: PatchNode, - ) -> impl Iterator + '_ { - self.value_ports(patch_node, Direction::Incoming) - .map(|(n, p)| (n, p.as_incoming().expect("unexpected port direction"))) + node @ PatchNode(commit_id, _): PatchNode, + ) -> Option { + let mut children = self.children_commits(commit_id); + children.find(move |&child_id| { + let child = self.get_commit(child_id); + child.deleted_parent_nodes().contains(&node) + }) } } -impl IntoIterator for PersistentHugr { - type Item = Commit; +impl<'a> IntoIterator for &'a PersistentHugr { + type Item = Commit<'a>; - type IntoIter = vec::IntoIter; + type IntoIter = vec::IntoIter>; fn into_iter(self) -> Self::IntoIter { - self.state_space - .all_commit_ids() - .map(|id| self.state_space.get_commit(id).clone()) + self.graph + .all_node_ids() + .map(|id| self.get_commit(id).clone()) .collect_vec() .into_iter() } @@ -597,36 +502,176 @@ impl IntoIterator for PersistentHugr { /// among `children`. pub(crate) fn find_conflicting_node<'a>( commit_id: CommitId, - children: impl IntoIterator, + children: impl IntoIterator>, ) -> Option { let mut all_invalidated = BTreeSet::new(); children.into_iter().find_map(|child| { let mut new_invalidated = child - .invalidation_set() + .deleted_parent_nodes() .filter_map(|PatchNode(del_commit_id, node)| { - if del_commit_id == commit_id { - Some(node) - } else { - None - } + (del_commit_id == commit_id).then_some(node) }); new_invalidated.find(|&n| !all_invalidated.insert(n)) }) } -fn get_parent_commits( - replacement: &PersistentReplacement, - graph: &CommitStateSpace, -) -> Result, InvalidCommit> { - let parent_ids = replacement.invalidation_set().map(|n| n.owner()).unique(); - parent_ids - .map(|id| { - graph - .try_get_commit(id) - .cloned() - .ok_or(InvalidCommit::UnknownParent(id)) - }) - .collect() +#[cfg(test)] +mod tests { + use super::*; + use hugr_core::{ + builder::{DFGBuilder, Dataflow, DataflowHugr, endo_sig}, + extension::prelude::bool_t, + hugr::views::SiblingSubgraph, + ops::handle::NodeHandle, + std_extensions::logic::LogicOp, + }; + use rstest::*; + + // two NOT gates + fn notop_hugr() -> (Hugr, Node, Node) { + let mut builder = DFGBuilder::new(endo_sig(vec![bool_t()])).unwrap(); + let [input] = builder.input_wires_arr(); + let notop = builder.add_dataflow_op(LogicOp::Not, [input]).unwrap(); + let notop2 = builder + .add_dataflow_op(LogicOp::Not, notop.outputs()) + .unwrap(); + let [out] = notop2.outputs_arr(); + ( + builder.finish_hugr_with_outputs([out]).unwrap(), + notop.node(), + notop2.node(), + ) + } + + fn add_commit(persistent_hugr: &mut PersistentHugr, node: PatchNode) -> (CommitId, PatchNode) { + let (repl_hugr, repl_not, _) = notop_hugr(); + let repl1 = PersistentReplacement::try_new( + SiblingSubgraph::from_node(node, &persistent_hugr), + &persistent_hugr, + repl_hugr, + ) + .unwrap(); + let commit = persistent_hugr.add_replacement(repl1); + (commit, PatchNode(commit, repl_not)) + } + + #[fixture] + fn linear_commits() -> (PersistentHugr, Vec) { + let (base_hugr, notop, _) = notop_hugr(); + + // Create a linear chain of commits: base -> commit1 -> commit2 -> commit3 + let mut persistent_hugr = PersistentHugr::with_base(base_hugr); + + let base_not = persistent_hugr.base_commit().to_patch_node(notop); + + // Create commit1 (child of base) + let (cm1, cm1_not) = add_commit(&mut persistent_hugr, base_not); + + // Create commit2 (child of commit1) + let (cm2, cm2_not) = add_commit(&mut persistent_hugr, cm1_not); + + // Create commit3 (child of commit2) + let (cm3, _cm3_not) = add_commit(&mut persistent_hugr, cm2_not); + + let base_id = persistent_hugr.base(); + (persistent_hugr, vec![base_id, cm1, cm2, cm3]) + } + + #[fixture] + fn branching_commits() -> (PersistentHugr, Vec) { + // Create a branching structure: base -> commit1 -> commit2 + // \-> commit3 -> commit4 + let (base_hugr, notop, notop2) = notop_hugr(); + let mut persistent_hugr = PersistentHugr::with_base(base_hugr); + + let base_commit = persistent_hugr.base_commit(); + let base_not = base_commit.to_patch_node(notop); + let base_not2 = base_commit.to_patch_node(notop2); + let base_id = base_commit.id(); + + // Create commit1 (child of base) + let (cm1, cm1_not) = add_commit(&mut persistent_hugr, base_not); + + // Create commit2 (child of commit1) + let (cm2, _cm2_not) = add_commit(&mut persistent_hugr, cm1_not); + + // Create commit3 (child of base) + let (cm3, cm3_not) = add_commit(&mut persistent_hugr, base_not2); + + // Create commit4 (child of commit3) + let (cm4, _cm4_not) = add_commit(&mut persistent_hugr, cm3_not); + + (persistent_hugr, vec![base_id, cm1, cm2, cm3, cm4]) + } + + #[rstest] + fn test_get_ancestors_while_linear_chain(linear_commits: (PersistentHugr, Vec)) { + let (persistent_hugr, commit_ids) = linear_commits; + let commits = commit_ids + .iter() + .map(|&id| persistent_hugr.get_commit(id).clone()) + .collect_vec(); + + // Starting from commit3, should get ancestors in reverse topological order + let ancestors = get_ancestors_while([commits[3].clone()], |_| true); + let ancestor_ids: Vec<_> = ancestors.iter().map(|c| c.id()).collect(); + + // Should be in reverse topological order: commit3, commit2, commit1, base + assert_eq!( + ancestor_ids, + vec![commit_ids[3], commit_ids[2], commit_ids[1], commit_ids[0]] + ); + } + + #[rstest] + fn test_get_ancestors_while_branching_structure( + branching_commits: (PersistentHugr, Vec), + ) { + let (persistent_hugr, commit_ids) = branching_commits; + let commits = commit_ids + .iter() + .map(|&id| persistent_hugr.get_commit(id).clone()) + .collect_vec(); + + // Starting from both commit2 and commit4, should get all ancestors + let ancestors = get_ancestors_while([commits[2].clone(), commits[4].clone()], |_| true); + let ancestor_ids: Vec<_> = ancestors.iter().map(|c| c.id()).collect(); + + // Should include all commits, with descendants before ancestors + let valid_orderings = [ + vec![ + commit_ids[4], + commit_ids[3], + commit_ids[2], + commit_ids[1], + commit_ids[0], + ], + vec![ + commit_ids[2], + commit_ids[1], + commit_ids[4], + commit_ids[3], + commit_ids[0], + ], + ]; + assert!(valid_orderings.contains(&ancestor_ids)); + } + + #[rstest] + fn test_get_ancestors_while_with_filter(linear_commits: (PersistentHugr, Vec)) { + let (persistent_hugr, commit_ids) = linear_commits; + let commits = commit_ids + .iter() + .map(|&id| persistent_hugr.get_commit(id).clone()) + .collect_vec(); + let [_base, commit1, commit2, commit3] = commits.try_into().unwrap(); + + // Use a filter that stops at commit1 + let ancestors = get_ancestors_while([commit3.clone()], |c| c.id() != commit1.id()); + let ancestor_ids: Vec<_> = ancestors.iter().map(|c| c.id()).collect(); + + assert_eq!(ancestor_ids, vec![commit3.id(), commit2.id(), commit1.id()]); + } } diff --git a/hugr-persistent/src/persistent_hugr/serial.rs b/hugr-persistent/src/persistent_hugr/serial.rs index 9a41e4acef..688ae39a59 100644 --- a/hugr-persistent/src/persistent_hugr/serial.rs +++ b/hugr-persistent/src/persistent_hugr/serial.rs @@ -1,74 +1,49 @@ //! Serialized format for [`PersistentHugr`] -use hugr_core::Hugr; - -use crate::{CommitStateSpace, Resolver, state_space::serial::SerialCommitStateSpace}; +use crate::{CommitId, CommitStateSpace}; use super::PersistentHugr; /// Serialized format for [`PersistentHugr`] #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SerialPersistentHugr { +pub struct SerialPersistentHugr { /// The state space of all commits. - state_space: SerialCommitStateSpace, + pub commits: Vec, } -impl PersistentHugr { +impl PersistentHugr { /// Create a new [`CommitStateSpace`] from its serialized format - pub fn from_serial>(value: SerialPersistentHugr) -> Self { - let SerialPersistentHugr { state_space } = value; - let state_space = CommitStateSpace::from_serial(state_space); - Self { state_space } + pub fn from_serial(value: SerialPersistentHugr, state_space: &CommitStateSpace) -> Self { + state_space.try_create(value.commits).unwrap() } /// Convert a [`CommitStateSpace`] into its serialized format - pub fn into_serial>(self) -> SerialPersistentHugr { - let Self { state_space } = self; - let state_space = state_space.into_serial(); - SerialPersistentHugr { state_space } - } - - /// Create a serialized format from a reference to [`CommitStateSpace`] - pub fn to_serial>(&self) -> SerialPersistentHugr { - let Self { state_space } = self; - let state_space = state_space.to_serial(); - SerialPersistentHugr { state_space } - } -} - -impl, R: Resolver> From> for SerialPersistentHugr { - fn from(value: PersistentHugr) -> Self { - value.into_serial() + pub fn to_serial(&self) -> SerialPersistentHugr { + SerialPersistentHugr { + commits: self.all_commit_ids().collect(), + } } } -impl, R: Resolver> From> for PersistentHugr { - fn from(value: SerialPersistentHugr) -> Self { - PersistentHugr::from_serial(value) +impl From for SerialPersistentHugr { + fn from(value: PersistentHugr) -> Self { + value.to_serial() } } #[cfg(test)] mod tests { use super::*; - use crate::{ - CommitId, SerdeHashResolver, - tests::{WrappedHugr, test_state_space}, - }; + use crate::tests::{TestStateSpace, test_state_space}; use rstest::rstest; #[rstest] - fn test_serde_persistent_hugr( - test_state_space: ( - CommitStateSpace>, - [CommitId; 4], - ), - ) { - let (state_space, [cm1, cm2, _, cm4]) = test_state_space; + fn test_serde_persistent_hugr(test_state_space: TestStateSpace) { + let [cm1, cm2, _, cm4] = test_state_space.commits(); - let per_hugr = state_space.try_extract_hugr([cm1, cm2, cm4]).unwrap(); - let ser_per_hugr = per_hugr.to_serial::(); + let per_hugr = PersistentHugr::try_new([cm1.clone(), cm2.clone(), cm4.clone()]).unwrap(); + let ser_per_hugr = per_hugr.to_serial(); insta::assert_snapshot!(serde_json::to_string_pretty(&ser_per_hugr).unwrap()); } diff --git a/hugr-persistent/src/persistent_hugr/snapshots/hugr_persistent__persistent_hugr__serial__tests__serde_persistent_hugr.snap b/hugr-persistent/src/persistent_hugr/snapshots/hugr_persistent__persistent_hugr__serial__tests__serde_persistent_hugr.snap index 4300130ad4..cced298174 100644 --- a/hugr-persistent/src/persistent_hugr/snapshots/hugr_persistent__persistent_hugr__serial__tests__serde_persistent_hugr.snap +++ b/hugr-persistent/src/persistent_hugr/snapshots/hugr_persistent__persistent_hugr__serial__tests__serde_persistent_hugr.snap @@ -3,185 +3,22 @@ source: hugr-persistent/src/persistent_hugr/serial.rs expression: "serde_json::to_string_pretty(&ser_per_hugr).unwrap()" --- { - "state_space": { - "graph": { - "nodes": { - "148755cb1c03248a": { - "value": { - "Replacement": { - "subgraph": { - "nodes": [ - [ - "47cffa745ac0d979", - 8 - ] - ], - "inputs": [ - [ - [ - [ - "47cffa745ac0d979", - 8 - ], - { - "index": 0 - } - ] - ] - ], - "outputs": [ - [ - [ - "47cffa745ac0d979", - 8 - ], - { - "index": 0 - } - ] - ], - "function_calls": [] - }, - "replacement": { - "hugr": "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[4,0],[3,0]],[[5,0],[7,0]],[[7,0],[8,0]],[[8,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" - } - } - }, - "incoming": [ - [ - "47cffa745ac0d979", - null - ] - ] - }, - "47cffa745ac0d979": { - "value": { - "Base": { - "hugr": "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"And\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[2,1],[4,1]],[[4,0],[3,0]],[[5,0],[7,0]],[[5,1],[8,0]],[[7,0],[9,0]],[[8,0],[9,1]],[[9,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" - } - }, - "incoming": [] - }, - "5fbd1e8d9e17af43": { - "value": { - "Replacement": { - "subgraph": { - "nodes": [ - [ - "47cffa745ac0d979", - 7 - ] - ], - "inputs": [ - [ - [ - [ - "47cffa745ac0d979", - 7 - ], - { - "index": 0 - } - ] - ] - ], - "outputs": [ - [ - [ - "47cffa745ac0d979", - 7 - ], - { - "index": 0 - } - ] - ], - "function_calls": [] - }, - "replacement": { - "hugr": "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[4,0],[3,0]],[[5,0],[7,0]],[[7,0],[8,0]],[[8,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" - } - } - }, - "incoming": [ - [ - "47cffa745ac0d979", - null - ] - ] - }, - "a0db71969c2eedfc": { - "value": { - "Replacement": { - "subgraph": { - "nodes": [ - [ - "47cffa745ac0d979", - 9 - ], - [ - "5fbd1e8d9e17af43", - 8 - ] - ], - "inputs": [ - [ - [ - [ - "5fbd1e8d9e17af43", - 8 - ], - { - "index": 0 - } - ] - ], - [ - [ - [ - "47cffa745ac0d979", - 9 - ], - { - "index": 1 - } - ] - ] - ], - "outputs": [ - [ - [ - "47cffa745ac0d979", - 9 - ], - { - "index": 0 - } - ] - ], - "function_calls": [] - }, - "replacement": { - "hugr": "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Xor\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[2,1],[4,1]],[[4,0],[3,0]],[[5,0],[7,0]],[[5,1],[7,1]],[[7,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" - } - } - }, - "incoming": [ - [ - "47cffa745ac0d979", - null - ], - [ - "5fbd1e8d9e17af43", - null - ] - ] - } - }, - "resolver_id": { - "id": "SerdeHashResolver" - } + "commits": [ + { + "idx": 1, + "version": 1 }, - "base_commit": "47cffa745ac0d979" - } + { + "idx": 2, + "version": 1 + }, + { + "idx": 3, + "version": 1 + }, + { + "idx": 5, + "version": 1 + } + ] } diff --git a/hugr-persistent/src/state_space.rs b/hugr-persistent/src/state_space.rs index 30f704347d..c61c7d79c7 100644 --- a/hugr-persistent/src/state_space.rs +++ b/hugr-persistent/src/state_space.rs @@ -1,34 +1,21 @@ -//! Store of commit histories for a [`PersistentHugr`]. +//! Store of commits that are refered to in [`PersistentHugr`]s. -use std::collections::{BTreeSet, VecDeque}; +use std::{ + cell::{Ref, RefCell}, + mem, + rc::Rc, +}; -use delegate::delegate; use derive_more::From; -use hugr_core::{ - Direction, Hugr, HugrView, IncomingPort, Node, OutgoingPort, Port, SimpleReplacement, - hugr::{ - self, - internal::HugrInternals, - patch::{ - BoundaryPort, - simple_replace::{BoundaryMode, InvalidReplacement}, - }, - views::InvalidSignature, - }, - ops::OpType, -}; -use itertools::{Either, Itertools}; -use relrc::{HistoryGraph, RelRc}; -use thiserror::Error; +use hugr_core::{Hugr, Node}; +use itertools::Itertools; +use relrc::Registry; -use crate::{ - Commit, PersistentHugr, PersistentReplacement, PointerEqResolver, Resolver, - find_conflicting_node, parents_view::ParentsView, subgraph::InvalidPinnedSubgraph, -}; +use crate::{Commit, InvalidCommit, PersistentHugr, PersistentReplacement}; pub mod serial; -/// A copyable handle to a [`Commit`] vertex within a [`CommitStateSpace`] +/// A copyable handle to a [`Commit`] vertex within a [`CommitStateSpace`]. pub type CommitId = relrc::NodeId; /// A HUGR node within a commit of the commit state space @@ -47,7 +34,7 @@ impl PatchNode { // Print out PatchNodes as `Node(x)@commit_hex` impl std::fmt::Debug for PatchNode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}@{}", self.1, self.0) + write!(f, "{:?}@{:?}", self.1, self.0) } } @@ -74,561 +61,164 @@ mod hidden { } pub(crate) use hidden::CommitData; -/// A set of commits with directed (acyclic) dependencies between them. -/// -/// Vertices in the [`CommitStateSpace`] are [`Commit`]s and there is an edge -/// from a commit `c1` to a commit `c2` if `c1` must be applied before `c2`: -/// in other words, if `c2` deletes nodes that are introduced in `c1`. We say -/// `c2` depends on (or is a child of) `c1`. +/// The set of all current commits, assigning every commit a unique ID. /// /// A [`CommitStateSpace`] always has a unique base commit (the root of the /// graph). All other commits are [`PersistentReplacement`]s that apply on top -/// of it. Commits are stored as [`RelRc`]s: they are reference-counted pointers -/// to the patch data that also maintain strong references to the commit's -/// parents. This means that commits can be cloned cheaply and dropped freely; -/// the memory of a commit will be released whenever no other commit in scope -/// depends on it. +/// of it. Commits are stored as [`relrc::RelRc`]s: they are reference-counted +/// pointers to the patch data that also maintain strong references to the +/// commit's parents. This means that commits can be cloned cheaply and dropped +/// freely; the memory of a commit will be released whenever no other commit in +/// scope depends on it. +/// +/// Note that a [`CommitStateSpace`] only keeps weak references to commits, so +/// it is invalid to keep commit IDs beyond the lifetime of the commit. IDs will +/// be invalidated as soon as the commits are dropped. /// /// Commits in a [`CommitStateSpace`] DO NOT represent a valid history in the /// general case: pairs of commits may be mutually exclusive if they modify the -/// same subgraph. Use [`Self::try_extract_hugr`] to get a [`PersistentHugr`] +/// same subgraph. Use [`Self::try_create`] to get a [`PersistentHugr`] /// with a set of compatible commits. +/// +/// Cloning a [`CommitStateSpace`] value corresponds to creating a new handle to +/// the same underlying state space. #[derive(Clone, Debug)] -pub struct CommitStateSpace { - /// A set of commits with directed (acyclic) dependencies between them. - /// - /// Each commit is stored as a [`RelRc`]. - pub(super) graph: HistoryGraph, - /// The unique root of the commit graph. - /// - /// The only commit in the graph with variant [`CommitData::Base`]. All - /// other commits are [`CommitData::Replacement`]s, and are descendants - /// of this. - pub(super) base_commit: CommitId, +#[repr(transparent)] +pub struct CommitStateSpace { + registry: Rc>>, } -impl CommitStateSpace { - /// Create a new commit state space with a single base commit. - pub fn with_base(hugr: Hugr) -> Self { - let commit = RelRc::new(CommitData::Base(hugr)); - let graph = HistoryGraph::new([commit.clone()], R::default()); - let base_commit = graph - .all_node_ids() - .exactly_one() - .ok() - .expect("graph has exactly one commit (just added)"); - Self { graph, base_commit } +impl PartialEq for CommitStateSpace { + fn eq(&self, other: &Self) -> bool { + self.registry.as_ptr() == other.registry.as_ptr() } +} - /// Create a new commit state space from a set of commits. - /// - /// Return a [`InvalidCommit::NonUniqueBase`] error if the commits do - /// not share a unique common ancestor base commit. - pub fn try_from_commits( - commits: impl IntoIterator, - ) -> Result { - let graph = HistoryGraph::new(commits.into_iter().map_into(), R::default()); - let base_commits = graph - .all_node_ids() - .filter(|&id| matches!(graph.get_node(id).value(), CommitData::Base(_))) - .collect_vec(); - let base_commit = base_commits - .into_iter() - .exactly_one() - .map_err(|err| InvalidCommit::NonUniqueBase(err.len()))?; - Ok(Self { graph, base_commit }) - } +impl Eq for CommitStateSpace {} - /// Add a replacement commit to the graph. - /// - /// Return an [`InvalidCommit::EmptyReplacement`] error if the replacement - /// is empty. - pub fn try_add_replacement( - &mut self, - replacement: PersistentReplacement, - ) -> Result { - let commit = Commit::try_from_replacement(replacement, self)?; - self.try_add_commit(commit) +impl From> for CommitStateSpace { + fn from(registry: Registry) -> Self { + Self { + registry: Rc::new(RefCell::new(registry)), + } } +} - /// Add a commit (and all its ancestors) to the state space. - /// - /// Returns an [`InvalidCommit::NonUniqueBase`] error if the commit is a - /// base commit and does not coincide with the existing base commit. - pub fn try_add_commit(&mut self, commit: Commit) -> Result { - let is_base = commit.as_relrc().ptr_eq(self.base_commit().as_relrc()); - if !is_base && matches!(commit.value(), CommitData::Base(_)) { - return Err(InvalidCommit::NonUniqueBase(2)); - } - let commit = commit.into(); - Ok(self.graph.insert_node(commit)) +impl From>>> for CommitStateSpace { + fn from(registry: Rc>>) -> Self { + Self { registry } } +} - /// Add a set of commits to the state space. - /// - /// Commits must be valid replacement commits or coincide with the existing - /// base commit. - pub fn extend(&mut self, commits: impl IntoIterator) { - // TODO: make this more efficient - for commit in commits { - self.try_add_commit(commit) - .expect("invalid commit in extend"); - } +impl<'a> From<&'a Rc>>> for &'a CommitStateSpace { + fn from(rc: &'a Rc>>) -> Self { + // SAFETY: Commit is a transparent wrapper around the registry Rc + unsafe { mem::transmute(rc) } } +} - /// Extract a `PersistentHugr` from this state space, consisting of - /// `commits` and their ancestors. - /// - /// All commits in the resulting `PersistentHugr` are guaranteed to be - /// compatible. If the selected commits would include two commits which - /// are incompatible, a [`InvalidCommit::IncompatibleHistory`] error is - /// returned. If `commits` is empty, a [`InvalidCommit::NonUniqueBase`] - /// error is returned. - pub fn try_extract_hugr( - &self, - commits: impl IntoIterator, - ) -> Result, InvalidCommit> { - // Define commits as the set of all ancestors of the given commits - let all_commit_ids = get_all_ancestors(&self.graph, commits); +impl Default for CommitStateSpace { + fn default() -> Self { + Self::new() + } +} - if all_commit_ids.is_empty() { - return Err(InvalidCommit::NonUniqueBase(0)); - } - debug_assert!(all_commit_ids.contains(&self.base())); +impl CommitStateSpace { + /// Create a new empty commit state space. + pub fn new() -> Self { + let registry = Rc::new(RefCell::new(Registry::new())); + Self { registry } + } - // Check that all commits are compatible - for &commit_id in &all_commit_ids { - let selected_children = self - .children(commit_id) - .filter(|id| all_commit_ids.contains(id)) - .map(|id| self.get_commit(id)); - if let Some(node) = find_conflicting_node(commit_id, selected_children) { - return Err(InvalidCommit::IncompatibleHistory(commit_id, node)); - } + /// Set the base commit of the state space. + /// + /// This will only succeed if the state space is currently empty + /// (otherwise a base commit already exists). + pub fn try_set_base(&self, hugr: Hugr) -> Option> { + if !self.registry.borrow().is_empty() { + return None; } - - let commits = all_commit_ids - .into_iter() - .map(|id| self.get_commit(id).as_relrc().clone()); - let subgraph = HistoryGraph::new(commits, R::default()); - - Ok(PersistentHugr::from_state_space_unsafe(Self { - graph: subgraph, - base_commit: self.base_commit, - })) + Some(Commit::new_base(hugr, self)) } -} -impl CommitStateSpace { /// Check if `commit` is in the commit state space. pub fn contains(&self, commit: &Commit) -> bool { - self.graph.contains(commit.as_relrc()) + self.borrow().contains(commit.as_relrc()) } /// Check if `commit_id` is in the commit state space. pub fn contains_id(&self, commit_id: CommitId) -> bool { - self.graph.contains_id(commit_id) + self.borrow().contains_id(commit_id) } - /// Get the base commit ID. - pub fn base(&self) -> CommitId { - self.base_commit + /// Get the ID of `commit` in the commit state space. + pub fn get_id(&self, commit: &Commit) -> Option { + self.borrow().get_id(commit.as_relrc()) } - /// Get the base [`Hugr`]. - pub fn base_hugr(&self) -> &Hugr { - let CommitData::Base(hugr) = self.graph.get_node(self.base_commit).value() else { - panic!("base commit is not a base hugr"); - }; - hugr - } - - /// Get the base commit. - pub fn base_commit(&self) -> &Commit { - self.get_commit(self.base_commit) - } - - /// Get the commit with ID `commit_id`. - pub fn get_commit(&self, commit_id: CommitId) -> &Commit { - self.graph.get_node(commit_id).into() - } - - /// Check whether `commit_id` exists and return it. - pub fn try_get_commit(&self, commit_id: CommitId) -> Option<&Commit> { - self.contains_id(commit_id) - .then(|| self.get_commit(commit_id)) - } - - /// Get an iterator over all commit IDs in the state space. - pub fn all_commit_ids(&self) -> impl Iterator + Clone + '_ { - let vec = self.graph.all_node_ids().collect_vec(); - vec.into_iter() - } - - /// Get an iterator over all nodes inserted by `commit_id`. + /// A reverse lookup to obtain the commit from `commit_id`. /// - /// All nodes will be PatchNodes with commit ID `commit_id`. - pub fn inserted_nodes(&self, commit_id: CommitId) -> impl Iterator + '_ { - let commit = self.get_commit(commit_id); - let to_patch_node = move |node| PatchNode(commit_id, node); - commit.inserted_nodes().map(to_patch_node) - } - - /// Get the set of nodes invalidated by `commit_id` in `parent`. - pub fn invalidation_set( - &self, - commit_id: CommitId, - parent: CommitId, - ) -> impl Iterator + '_ { - let commit = self.get_commit(commit_id); - let ret = commit - .invalidation_set() - .filter(move |n| n.0 == parent) - .map(|n| n.1); - Some(ret).into_iter().flatten() + /// Will return `None` if the commit_id does not exist in the registry or + /// if it has already been dropped. + pub fn try_upgrade<'a>(&'a self, commit_id: CommitId) -> Option> { + self.borrow() + .get(commit_id) + // SAFETY: the commit will be alive as long as the state space + // is alive + .map(|rc| unsafe { Commit::from_relrc(rc) }) } - delegate! { - to self.graph { - /// Get the parents of `commit_id` - pub fn parents(&self, commit_id: CommitId) -> impl Iterator + '_; - /// Get the children of `commit_id` - pub fn children(&self, commit_id: CommitId) -> impl Iterator + '_; - } + fn borrow(&self) -> Ref<'_, Registry> { + self.registry.as_ref().borrow() } - pub(crate) fn as_history_graph(&self) -> &HistoryGraph { - &self.graph + pub fn as_registry(&self) -> &Rc>> { + &self.registry } - /// Get the Hugr inserted by `commit_id`. - /// - /// This is either the replacement Hugr of a [`CommitData::Replacement`] or - /// the base Hugr of a [`CommitData::Base`]. - pub(crate) fn commit_hugr(&self, commit_id: CommitId) -> &Hugr { - let commit = self.get_commit(commit_id); - match commit.value() { - CommitData::Base(base) => base, - CommitData::Replacement(repl) => repl.replacement(), - } + pub fn to_registry(&self) -> Rc>> { + self.registry.clone() } - /// Whether the edge at `(node, port)` is a boundary edge of `child`. - /// - /// Check if `(node, port)` is outside of the subgraph of the patch of child - /// and at least one opposite node is inside the subgraph. - pub fn is_boundary_edge( - &self, - node: PatchNode, - port: impl Into, - child: CommitId, - ) -> bool { - let deleted_nodes: BTreeSet<_> = self.get_commit(child).deleted_nodes().collect(); - if deleted_nodes.contains(&node) { - return false; - } - let mut opp_nodes = self - .commit_hugr(node.0) - .linked_ports(node.1, port) - .map(|(n, _)| PatchNode(node.0, n)); - opp_nodes.any(|n| deleted_nodes.contains(&n)) + /// Get all commits in the state space as a vector. + pub fn all_commits(&self) -> Vec<(CommitId, Commit<'_>)> { + self.borrow() + .iter() + .map(|(id, rc)| (id, unsafe { Commit::from_relrc(rc) })) + .collect() } - /// Get the boundary inputs linked to `(node, port)` in `child`. - /// - /// The returned ports will be ports on successors of the input node in the - /// `child` commit, unless (node, port) is connected to a passthrough wire - /// in `child` (i.e. a wire from input node to output node), in which - /// case they will be in one of the parents of `child`. - /// - /// `child` should be a child commit of the owner of `node`. - /// - /// ## Panics + /// Create a new [`PersistentHugr`] in this state space, consisting of + /// `commits` and their ancestors. /// - /// Panics if `(node, port)` is not a boundary edge, if `child` is not - /// a valid commit ID or if it is the base commit. - pub(crate) fn linked_child_inputs( + /// All commits in the resulting `PersistentHugr` are guaranteed to be + /// compatible. If the selected commits would include two commits which + /// are incompatible, a [`InvalidCommit::IncompatibleHistory`] error is + /// returned. If `commits` is empty, a [`InvalidCommit::NonUniqueBase`] + /// error is returned. + pub fn try_create( &self, - node: PatchNode, - port: OutgoingPort, - child: CommitId, - return_invalid: BoundaryMode, - ) -> impl Iterator + '_ { - assert!( - self.is_boundary_edge(node, port, child), - "not a boundary edge" - ); - - let parent_hugrs = ParentsView::from_commit(child, self); - let repl = self.replacement(child).expect("valid child commit"); - repl.linked_replacement_inputs((node, port), &parent_hugrs, return_invalid) - .collect_vec() + commits: impl IntoIterator, + ) -> Result { + let commits: Vec<_> = commits .into_iter() - .map(move |np| match np { - BoundaryPort::Host(patch_node, port) => (patch_node, port), - BoundaryPort::Replacement(node, port) => (PatchNode(child, node), port), + .map(|id| { + self.try_upgrade(id) + .ok_or(InvalidCommit::UnknownCommitId(id)) }) + .try_collect()?; + PersistentHugr::try_new(commits) } - /// Get the single boundary output linked to `(node, port)` in `child`. + /// Get the (unique) base commit of the state space. /// - /// The returned port will be a port on a predecessor of the output node in - /// the `child` commit, unless (node, port) is connected to a passthrough - /// wire in `child` (i.e. a wire from input node to output node), in - /// which case it will be in one of the parents of `child`. - /// - /// `child` should be a child commit of the owner of `node` (or `None` will - /// be returned). - /// - /// ## Panics - /// - /// Panics if `child` is not a valid commit ID. - pub(crate) fn linked_child_output( - &self, - node: PatchNode, - port: IncomingPort, - child: CommitId, - return_invalid: BoundaryMode, - ) -> Option<(PatchNode, OutgoingPort)> { - let parent_hugrs = ParentsView::from_commit(child, self); - let repl = self.replacement(child)?; - match repl.linked_replacement_output((node, port), &parent_hugrs, return_invalid)? { - BoundaryPort::Host(patch_node, port) => (patch_node, port), - BoundaryPort::Replacement(node, port) => (PatchNode(child, node), port), - } - .into() - } - - /// Get the boundary ports linked to `(node, port)` in `child`. - /// - /// `child` should be a child commit of the owner of `node`. - /// - /// See [`Self::linked_child_inputs`] and [`Self::linked_child_output`] for - /// more details. - pub(crate) fn linked_child_ports( - &self, - node: PatchNode, - port: impl Into, - child: CommitId, - return_invalid: BoundaryMode, - ) -> impl Iterator + '_ { - match port.into().as_directed() { - Either::Left(incoming) => Either::Left( - self.linked_child_output(node, incoming, child, return_invalid) - .into_iter() - .map(|(node, port)| (node, port.into())), - ), - Either::Right(outgoing) => Either::Right( - self.linked_child_inputs(node, outgoing, child, return_invalid) - .map(|(node, port)| (node, port.into())), - ), - } - } - - /// Get the single output port linked to `(node, port)` in a parent of the - /// commit of `node`. - /// - /// The returned port belongs to the input boundary of the subgraph in - /// parent. - /// - /// ## Panics - /// - /// Panics if `(node, port)` is not connected to the input node in the - /// commit of `node`, or if the node is not valid. - pub fn linked_parent_input( - &self, - PatchNode(commit_id, node): PatchNode, - port: IncomingPort, - ) -> (PatchNode, OutgoingPort) { - let repl = self.replacement(commit_id).expect("valid commit"); - - assert!( - repl.replacement() - .input_neighbours(node) - .contains(&repl.get_replacement_io()[0]), - "not connected to input" - ); - - let parent_hugrs = ParentsView::from_commit(commit_id, self); - repl.linked_host_input((node, port), &parent_hugrs).into() - } - - /// Get the input ports linked to `(node, port)` in a parent of the commit - /// of `node`. - /// - /// The returned ports belong to the output boundary of the subgraph in - /// parent. - /// - /// ## Panics - /// - /// Panics if `(node, port)` is not connected to the output node in the - /// commit of `node`, or if the node is not valid. - pub fn linked_parent_outputs( - &self, - PatchNode(commit_id, node): PatchNode, - port: OutgoingPort, - ) -> impl Iterator + '_ { - let repl = self.replacement(commit_id).expect("valid commit"); - - assert!( - repl.replacement() - .output_neighbours(node) - .contains(&repl.get_replacement_io()[1]), - "not connected to output" - ); - - let parent_hugrs = ParentsView::from_commit(commit_id, self); - repl.linked_host_outputs((node, port), &parent_hugrs) - .map_into() - .collect_vec() - .into_iter() - } - - /// Get the ports linked to `(node, port)` in a parent of the commit of - /// `node`. - /// - /// See [`Self::linked_parent_input`] and [`Self::linked_parent_outputs`] - /// for more details. - /// - /// ## Panics - /// - /// Panics if `(node, port)` is not connected to an IO node in the commit - /// of `node`, or if the node is not valid. - pub fn linked_parent_ports( - &self, - node: PatchNode, - port: impl Into, - ) -> impl Iterator + '_ { - match port.into().as_directed() { - Either::Left(incoming) => { - let (node, port) = self.linked_parent_input(node, incoming); - Either::Left(std::iter::once((node, port.into()))) - } - Either::Right(outgoing) => Either::Right( - self.linked_parent_outputs(node, outgoing) - .map(|(node, port)| (node, port.into())), - ), - } + /// Return `None` if `self` is empty. + pub fn base_commit<'a>(&'a self) -> Option> { + // get any commit + let (_, relrc) = self.borrow().iter().next()?; + // SAFETY: commit will be alive as long as the state space + // is alive + let commit: Commit<'a> = unsafe { Commit::from_relrc(relrc) }; + Some(commit.base_commit().clone()) } - - /// Get the replacement for `commit_id`. - pub(crate) fn replacement(&self, commit_id: CommitId) -> Option<&SimpleReplacement> { - let commit = self.get_commit(commit_id); - commit.replacement() - } -} - -// The subset of HugrView methods that can be implemented on CommitStateSpace -// by simplify delegating to the patches' respective HUGRs -impl CommitStateSpace { - /// Get the type of the operation at `node`. - pub fn get_optype(&self, PatchNode(commit_id, node): PatchNode) -> &OpType { - let hugr = self.commit_hugr(commit_id); - hugr.get_optype(node) - } - - /// Get the number of ports of `node` in `dir`. - pub fn num_ports(&self, PatchNode(commit_id, node): PatchNode, dir: Direction) -> usize { - self.commit_hugr(commit_id).num_ports(node, dir) - } - - /// Iterator over output ports of node. - /// Like [`CommitStateSpace::node_ports`](node, Direction::Outgoing)` - /// but preserves knowledge that the ports are [OutgoingPort]s. - #[inline] - pub fn node_outputs(&self, node: PatchNode) -> impl Iterator + Clone + '_ { - self.node_ports(node, Direction::Outgoing) - .map(|p| p.as_outgoing().unwrap()) - } - - /// Iterator over inputs ports of node. - /// Like [`CommitStateSpace::node_ports`](node, Direction::Incoming)` - /// but preserves knowledge that the ports are [IncomingPort]s. - #[inline] - pub fn node_inputs(&self, node: PatchNode) -> impl Iterator + Clone + '_ { - self.node_ports(node, Direction::Incoming) - .map(|p| p.as_incoming().unwrap()) - } - - /// Get an iterator over the ports of `node` in `dir`. - pub fn node_ports( - &self, - PatchNode(commit_id, node): PatchNode, - dir: Direction, - ) -> impl Iterator + Clone + '_ { - self.commit_hugr(commit_id).node_ports(node, dir) - } - - /// Get an iterator over all ports of `node`. - pub fn all_node_ports( - &self, - PatchNode(commit_id, node): PatchNode, - ) -> impl Iterator + Clone + '_ { - self.commit_hugr(commit_id).all_node_ports(node) - } - - /// Get the metadata map of `node`. - pub fn node_metadata_map( - &self, - PatchNode(commit_id, node): PatchNode, - ) -> &hugr::NodeMetadataMap { - self.commit_hugr(commit_id).node_metadata_map(node) - } -} - -fn get_all_ancestors( - graph: &HistoryGraph, - commits: impl IntoIterator, -) -> BTreeSet { - let mut queue = VecDeque::from_iter(commits); - let mut ancestors = BTreeSet::from_iter(queue.iter().copied()); - while let Some(commit_id) = queue.pop_front() { - for parent in graph.parents(commit_id) { - if ancestors.insert(parent) { - queue.push_back(parent); - } - } - } - ancestors -} - -/// An error that occurs when trying to add a commit to a commit state space. -#[derive(Debug, Clone, Error)] -#[non_exhaustive] -pub enum InvalidCommit { - /// The commit conflicts with existing commits in the state space. - #[error("Incompatible history: children of commit {0:?} conflict in {1:?}")] - IncompatibleHistory(CommitId, Node), - - /// The commit has a parent not present in the state space. - #[error("Missing parent commit: {0:?}")] - UnknownParent(CommitId), - - /// The commit is not a replacement. - #[error("Commit is not a replacement")] - NotReplacement, - - /// The set of commits contains zero or more than one base commit. - #[error("{0} base commits found (should be 1)")] - NonUniqueBase(usize), - - /// The commit is an empty replacement. - #[error("Not allowed: empty replacement")] - EmptyReplacement, - - #[error("Invalid subgraph: {0}")] - /// The subgraph of the replacement is not convex. - InvalidSubgraph(#[from] InvalidPinnedSubgraph), - - /// The replacement of the commit is invalid. - #[error("Invalid replacement: {0}")] - InvalidReplacement(#[from] InvalidReplacement), - - /// The signature of the replacement is invalid. - #[error("Invalid signature: {0}")] - InvalidSignature(#[from] InvalidSignature), - - /// A wire has an unpinned port. - #[error("Incomplete wire: {0} is unpinned")] - IncompleteWire(PatchNode, Port), } diff --git a/hugr-persistent/src/state_space/serial.rs b/hugr-persistent/src/state_space/serial.rs index b20c585eb7..5ebbdf7ebf 100644 --- a/hugr-persistent/src/state_space/serial.rs +++ b/hugr-persistent/src/state_space/serial.rs @@ -1,6 +1,8 @@ //! Serialized format for [`CommitStateSpace`] -use relrc::serialization::SerializedHistoryGraph; +use relrc::serialization::SerializedRegistry; + +use crate::serial::SerialPersistentHugr; use super::*; use hugr_core::hugr::patch::simple_replace::serial::SerialSimpleReplacement; @@ -51,88 +53,115 @@ impl> From> for CommitData { } } -/// Serialized format for commit state space -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SerialCommitStateSpace { - /// The serialized history graph containing commit data - pub graph: SerializedHistoryGraph, (), R>, - /// The base commit ID - pub base_commit: CommitId, +/// Serialize a [`CommitStateSpace`] alongside a set of [`PersistentHugr`]s in +/// that space. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SerialCommitStateSpace { + pub registry: SerializedRegistry, ()>, + pub hugrs: Vec, } -impl CommitStateSpace { - /// Create a new [`CommitStateSpace`] from its serialized format - pub fn from_serial>(value: SerialCommitStateSpace) -> Self { - let SerialCommitStateSpace { graph, base_commit } = value; - - // Deserialize the SerializedHistoryGraph into a HistoryGraph with CommitData - let graph = graph.map_nodes(|n| CommitData::from_serial(n)); - let graph = HistoryGraph::try_from_serialized(graph, R::default()) - .expect("failed to deserialize history graph"); - - Self { graph, base_commit } +impl SerialCommitStateSpace { + pub fn new(state_space: &CommitStateSpace) -> Self + where + H: From, + { + let registry = state_space.as_registry().borrow().to_serialized(); + let registry = registry.map_nodes(|n| n.into_serial()); + Self { + registry, + hugrs: Vec::new(), + } } - /// Convert a [`CommitStateSpace`] into its serialized format - pub fn into_serial>(self) -> SerialCommitStateSpace { - let Self { graph, base_commit } = self; - let graph = graph.to_serialized(); - let graph = graph.map_nodes(|n| n.into_serial()); - SerialCommitStateSpace { graph, base_commit } + pub fn add_hugr(&mut self, hugr: PersistentHugr) { + self.hugrs.push(hugr.to_serial()); } - /// Create a serialized format from a reference to [`CommitStateSpace`] - pub fn to_serial>(&self) -> SerialCommitStateSpace { - let Self { graph, base_commit } = self; - let graph = graph.to_serialized(); - let graph = graph.map_nodes(|n| n.into_serial()); - SerialCommitStateSpace { - graph, - base_commit: *base_commit, + pub fn deserialize_into_hugrs(self) -> Vec + where + H: Clone + Into, + { + let registry = self.registry.map_nodes(|n| CommitData::from_serial(n)); + let (registry, all_relrcs) = Registry::from_serialized(registry); + let state_space = CommitStateSpace::from(registry); + for (exp_node, rc) in &all_relrcs { + let node = rc + .try_register_in(state_space.as_registry()) + .expect("deserialised rc is not registered"); + debug_assert_eq!( + exp_node, node, + "a new node ID was assigned to a node already in registry" + ); } + self.hugrs + .into_iter() + .map(|h| PersistentHugr::from_serial(h, &state_space)) + .collect() } } -impl, R: Resolver> From> for SerialCommitStateSpace { - fn from(value: CommitStateSpace) -> Self { - value.into_serial() +impl PersistentHugr { + /// Create a new [`CommitStateSpace`] from its serialized format + pub fn from_serial_state_space>( + value: SerialCommitStateSpace, + ) -> Vec { + value.deserialize_into_hugrs() } } -impl, R: Resolver> From> for CommitStateSpace { - fn from(value: SerialCommitStateSpace) -> Self { - CommitStateSpace::from_serial(value) +impl CommitStateSpace { + /// Convert a [`CommitStateSpace`] into its serialized format + pub fn to_serial>(&self) -> SerialCommitStateSpace { + SerialCommitStateSpace::new(self) } } #[cfg(test)] mod tests { + use std::collections::BTreeSet; + use rstest::rstest; use super::*; - use crate::{ - SerdeHashResolver, - tests::{WrappedHugr, test_state_space}, - }; + use crate::tests::{TestStateSpace, WrappedHugr, test_state_space}; #[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri #[rstest] - fn test_serialize_state_space( - test_state_space: ( - CommitStateSpace>, - [CommitId; 4], - ), - ) { - let (state_space, _) = test_state_space; - let serialized = state_space.to_serial::(); - - let deser = CommitStateSpace::from_serial(serialized.clone()); - let serialized_2 = deser.to_serial::(); - - insta::assert_snapshot!(serde_json::to_string_pretty(&serialized).unwrap()); + fn test_serialize_state_space(test_state_space: TestStateSpace) { + let commits: &[_; 4] = test_state_space.commits(); + let state_space = commits[0].state_space(); + let base_id = state_space.base_commit().unwrap().id(); + let mut ser_state_space = state_space.to_serial::(); + + let deser = PersistentHugr::from_serial_state_space(ser_state_space.clone()); + assert!(deser.is_empty()); + + let cm_set1 = commits[..2].to_owned(); + let cm_set2 = commits[..2] + .iter() + .chain([&commits[3]]) + .cloned() + .collect_vec(); + + ser_state_space.add_hugr(PersistentHugr::try_new(cm_set1.clone()).unwrap()); + ser_state_space.add_hugr(PersistentHugr::try_new(cm_set2.clone()).unwrap()); + + let deser = PersistentHugr::from_serial_state_space(ser_state_space.clone()); + + let [first, second] = deser.as_slice() else { + panic!("there should be two deserialized hugrs") + }; assert_eq!( - serde_json::to_string(&serialized).unwrap(), - serde_json::to_string(&serialized_2).unwrap() + first.all_commit_ids().collect::>(), + BTreeSet::from_iter(cm_set1.iter().map(|c| c.id()).chain([base_id])) ); + + assert_eq!( + second.all_commit_ids().collect::>(), + BTreeSet::from_iter(cm_set2.iter().map(|c| c.id()).chain([base_id])) + ); + + insta::assert_yaml_snapshot!(ser_state_space); } } diff --git a/hugr-persistent/src/state_space/snapshots/hugr_persistent__state_space__serial__tests__serialize_state_space.snap b/hugr-persistent/src/state_space/snapshots/hugr_persistent__state_space__serial__tests__serialize_state_space.snap index 1177107abb..08e213e7fb 100644 --- a/hugr-persistent/src/state_space/snapshots/hugr_persistent__state_space__serial__tests__serialize_state_space.snap +++ b/hugr-persistent/src/state_space/snapshots/hugr_persistent__state_space__serial__tests__serialize_state_space.snap @@ -1,248 +1,152 @@ --- source: hugr-persistent/src/state_space/serial.rs -expression: "serde_json::to_string_pretty(&serialized).unwrap()" +expression: ser_state_space --- -{ - "graph": { - "nodes": { - "148755cb1c03248a": { - "value": { - "Replacement": { - "subgraph": { - "nodes": [ - [ - "47cffa745ac0d979", - 8 - ] - ], - "inputs": [ - [ - [ - [ - "47cffa745ac0d979", - 8 - ], - { - "index": 0 - } - ] - ] - ], - "outputs": [ - [ - [ - "47cffa745ac0d979", - 8 - ], - { - "index": 0 - } - ] - ], - "function_calls": [] - }, - "replacement": { - "hugr": "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[4,0],[3,0]],[[5,0],[7,0]],[[7,0],[8,0]],[[8,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" - } - } - }, - "incoming": [ - [ - "47cffa745ac0d979", - null - ] - ] - }, - "47cffa745ac0d979": { - "value": { - "Base": { - "hugr": "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"And\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[2,1],[4,1]],[[4,0],[3,0]],[[5,0],[7,0]],[[5,1],[8,0]],[[7,0],[9,0]],[[8,0],[9,1]],[[9,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" - } - }, - "incoming": [] - }, - "5fbd1e8d9e17af43": { - "value": { - "Replacement": { - "subgraph": { - "nodes": [ - [ - "47cffa745ac0d979", - 7 - ] - ], - "inputs": [ - [ - [ - [ - "47cffa745ac0d979", - 7 - ], - { - "index": 0 - } - ] - ] - ], - "outputs": [ - [ - [ - "47cffa745ac0d979", - 7 - ], - { - "index": 0 - } - ] - ], - "function_calls": [] - }, - "replacement": { - "hugr": "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[4,0],[3,0]],[[5,0],[7,0]],[[7,0],[8,0]],[[8,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" - } - } - }, - "incoming": [ - [ - "47cffa745ac0d979", - null - ] - ] - }, - "a0db71969c2eedfc": { - "value": { - "Replacement": { - "subgraph": { - "nodes": [ - [ - "47cffa745ac0d979", - 9 - ], - [ - "5fbd1e8d9e17af43", - 8 - ] - ], - "inputs": [ - [ - [ - [ - "5fbd1e8d9e17af43", - 8 - ], - { - "index": 0 - } - ] - ], - [ - [ - [ - "47cffa745ac0d979", - 9 - ], - { - "index": 1 - } - ] - ] - ], - "outputs": [ - [ - [ - "47cffa745ac0d979", - 9 - ], - { - "index": 0 - } - ] - ], - "function_calls": [] - }, - "replacement": { - "hugr": "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Xor\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[2,1],[4,1]],[[4,0],[3,0]],[[5,0],[7,0]],[[5,1],[7,1]],[[7,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" - } - } - }, - "incoming": [ - [ - "47cffa745ac0d979", - null - ], - [ - "5fbd1e8d9e17af43", - null - ] - ] - }, - "bd65a3e461789320": { - "value": { - "Replacement": { - "subgraph": { - "nodes": [ - [ - "47cffa745ac0d979", - 7 - ], - [ - "47cffa745ac0d979", - 9 - ] - ], - "inputs": [ - [ - [ - [ - "47cffa745ac0d979", - 7 - ], - { - "index": 0 - } - ] - ], - [ - [ - [ - "47cffa745ac0d979", - 9 - ], - { - "index": 1 - } - ] - ] - ], - "outputs": [ - [ - [ - "47cffa745ac0d979", - 9 - ], - { - "index": 0 - } - ] - ], - "function_calls": [] - }, - "replacement": { - "hugr": "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Xor\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[2,1],[4,1]],[[4,0],[3,0]],[[5,0],[7,0]],[[5,1],[7,1]],[[7,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" - } - } - }, - "incoming": [ - [ - "47cffa745ac0d979", - null - ] - ] - } - }, - "resolver_id": { - "id": "SerdeHashResolver" - } - }, - "base_commit": "47cffa745ac0d979" -} +registry: + nodes: + - value: ~ + version: 0 + - value: + value: + Base: + hugr: "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"And\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[2,1],[4,1]],[[4,0],[3,0]],[[5,0],[7,0]],[[5,1],[8,0]],[[7,0],[9,0]],[[8,0],[9,1]],[[9,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" + incoming: [] + version: 1 + - value: + value: + Replacement: + subgraph: + nodes: + - - idx: 1 + version: 1 + - 7 + inputs: + - - - - idx: 1 + version: 1 + - 7 + - index: 0 + outputs: + - - - idx: 1 + version: 1 + - 7 + - index: 0 + function_calls: [] + replacement: + hugr: "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[4,0],[3,0]],[[5,0],[7,0]],[[7,0],[8,0]],[[8,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" + incoming: + - - idx: 1 + version: 1 + - ~ + version: 1 + - value: + value: + Replacement: + subgraph: + nodes: + - - idx: 1 + version: 1 + - 9 + - - idx: 2 + version: 1 + - 8 + inputs: + - - - - idx: 2 + version: 1 + - 8 + - index: 0 + - - - - idx: 1 + version: 1 + - 9 + - index: 1 + outputs: + - - - idx: 1 + version: 1 + - 9 + - index: 0 + function_calls: [] + replacement: + hugr: "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Xor\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[2,1],[4,1]],[[4,0],[3,0]],[[5,0],[7,0]],[[5,1],[7,1]],[[7,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" + incoming: + - - idx: 1 + version: 1 + - ~ + - - idx: 2 + version: 1 + - ~ + version: 1 + - value: + value: + Replacement: + subgraph: + nodes: + - - idx: 1 + version: 1 + - 7 + - - idx: 1 + version: 1 + - 9 + inputs: + - - - - idx: 1 + version: 1 + - 7 + - index: 0 + - - - - idx: 1 + version: 1 + - 9 + - index: 1 + outputs: + - - - idx: 1 + version: 1 + - 9 + - index: 0 + function_calls: [] + replacement: + hugr: "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Xor\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2},{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[2,1],[4,1]],[[4,0],[3,0]],[[5,0],[7,0]],[[5,1],[7,1]],[[7,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" + incoming: + - - idx: 1 + version: 1 + - ~ + version: 1 + - value: + value: + Replacement: + subgraph: + nodes: + - - idx: 1 + version: 1 + - 8 + inputs: + - - - - idx: 1 + version: 1 + - 8 + - index: 0 + outputs: + - - - idx: 1 + version: 1 + - 8 + - index: 0 + function_calls: [] + replacement: + hugr: "HUGRiHJv?@{\"modules\":[{\"version\":\"live\",\"nodes\":[{\"parent\":0,\"op\":\"Module\"},{\"parent\":0,\"op\":\"FuncDefn\",\"name\":\"main\",\"signature\":{\"params\":[],\"body\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},\"visibility\":\"Private\"},{\"parent\":1,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":1,\"op\":\"DFG\",\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Input\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Output\",\"types\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}},{\"parent\":4,\"op\":\"Extension\",\"extension\":\"logic\",\"name\":\"Not\",\"args\":[],\"signature\":{\"input\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}],\"output\":[{\"t\":\"Sum\",\"s\":\"Unit\",\"size\":2}]}}],\"edges\":[[[2,0],[4,0]],[[4,0],[3,0]],[[5,0],[7,0]],[[7,0],[8,0]],[[8,0],[6,0]]],\"metadata\":[null,null,null,null,null,null,null,null,null],\"entrypoint\":4}],\"extensions\":[]}" + incoming: + - - idx: 1 + version: 1 + - ~ + version: 1 +hugrs: + - commits: + - idx: 1 + version: 1 + - idx: 2 + version: 1 + - idx: 3 + version: 1 + - commits: + - idx: 1 + version: 1 + - idx: 2 + version: 1 + - idx: 3 + version: 1 + - idx: 5 + version: 1 diff --git a/hugr-persistent/src/subgraph.rs b/hugr-persistent/src/subgraph.rs index 7059f8899f..99e1e440f5 100644 --- a/hugr-persistent/src/subgraph.rs +++ b/hugr-persistent/src/subgraph.rs @@ -10,7 +10,7 @@ use hugr_core::{ use itertools::Itertools; use thiserror::Error; -use crate::{CommitId, PatchNode, PersistentHugr, PersistentWire, Resolver, Walker}; +use crate::{CommitId, PatchNode, PersistentHugr, PersistentWire, Walker}; /// A set of pinned nodes and wires between them, along with a fixed input /// and output boundary, simmilar to [`SiblingSubgraph`]. @@ -73,10 +73,10 @@ impl PinnedSubgraph { /// Nodes that are not isolated, i.e. are attached to at least one wire in /// `wires` will be added implicitly to the graph and do not need to be /// explicitly listed in `nodes`. - pub fn try_from_pinned( + pub fn try_from_pinned( nodes: impl IntoIterator, wires: impl IntoIterator, - walker: &Walker, + walker: &Walker, ) -> Result { let mut selected_commits = BTreeSet::new(); let host = walker.as_hugr_view(); @@ -111,9 +111,9 @@ impl PinnedSubgraph { } /// Create a new subgraph from a set of complete wires in `walker`. - pub fn try_from_wires( + pub fn try_from_wires( wires: impl IntoIterator, - walker: &Walker, + walker: &Walker, ) -> Result { Self::try_from_pinned(std::iter::empty(), wires, walker) } @@ -122,10 +122,10 @@ impl PinnedSubgraph { /// /// Return the input boundary ports, output boundary ports as well as the /// set of all nodes in the subgraph. - pub fn compute_io_ports( + pub fn compute_io_ports( nodes: impl IntoIterator, wires: impl IntoIterator, - host: &PersistentHugr, + host: &PersistentHugr, ) -> ( IncomingPorts, OutgoingPorts, @@ -147,13 +147,23 @@ impl PinnedSubgraph { // that are part of the wires let inputs = all_nodes .iter() - .flat_map(|&n| host.input_value_ports(n)) + .flat_map(|&PatchNode(owner, node)| { + let owner = host.get_commit(owner); + owner + .input_value_ports(node) + .map(|(n, p)| (owner.to_patch_node(n), p)) + }) .filter(|node_port| !wire_ports_incoming.contains(node_port)) .map(|np| vec![np]) .collect_vec(); let outputs = all_nodes .iter() - .flat_map(|&n| host.output_value_ports(n)) + .flat_map(|&PatchNode(owner, node)| { + let owner = host.get_commit(owner); + owner + .output_value_ports(node) + .map(|(n, p)| (owner.to_patch_node(n), p)) + }) .filter(|node_port| !wire_ports_outgoing.contains(node_port)) .collect_vec(); @@ -166,9 +176,9 @@ impl PinnedSubgraph { /// This will fail if any of the required selected commits are not in the /// host, if any of the nodes are invalid in the host (e.g. deleted by /// another commit in host), or if the subgraph is not convex. - pub fn to_sibling_subgraph( + pub fn to_sibling_subgraph( &self, - host: &PersistentHugr, + host: &PersistentHugr, ) -> Result, InvalidPinnedSubgraph> { if let Some(&unselected) = self .selected_commits @@ -217,7 +227,7 @@ impl PinnedSubgraph { pub enum InvalidPinnedSubgraph { #[error("Invalid subgraph: {0}")] InvalidSubgraph(#[from] InvalidSubgraph), - #[error("Invalid commit in host: {0}")] + #[error("Invalid commit in host: {0:?}")] InvalidCommit(CommitId), #[error("Wire is not complete: {0:?}")] IncompleteWire(PersistentWire), diff --git a/hugr-persistent/src/tests.rs b/hugr-persistent/src/tests.rs index 8e2e931659..dede74515c 100644 --- a/hugr-persistent/src/tests.rs +++ b/hugr-persistent/src/tests.rs @@ -12,7 +12,7 @@ use hugr_core::{ use rstest::*; use crate::{ - Commit, CommitStateSpace, PatchNode, PersistentHugr, PersistentReplacement, Resolver, + Commit, CommitStateSpace, PatchNode, PersistentHugr, PersistentReplacement, state_space::CommitId, }; @@ -142,7 +142,42 @@ fn create_not_and_to_xor_replacement(hugr: &Hugr) -> SimpleReplacement { SimpleReplacement::try_new(subgraph, hugr, replacement_hugr).unwrap() } -/// Creates a state space with 4 commits on top of the base hugr `simple_hugr`: +/// A state space for testing purposes, along with commits within it. +pub struct TestStateSpace { + #[allow(dead_code)] // this needs to stay in scope for the commits to be alive + state_space: CommitStateSpace, + // these commits typically borrow from the state space -- we replace the + // lifetime with a dummy static lifetime, fixed in the getter methods + commits: Vec>, +} + +impl TestStateSpace { + fn new<'a>(state_space: CommitStateSpace, commits: Vec>) -> Self { + assert!(commits.iter().all(|c| c.state_space() == state_space)); + let commits = commits + .into_iter() + // SAFETY: the commits will be alive as long as the state space is + // alive + .map(|c| unsafe { c.upgrade_lifetime() }) + .collect(); + Self { + state_space, + commits, + } + } + + #[allow(dead_code)] + pub fn state_space(&self) -> &CommitStateSpace { + &self.state_space + } + + pub fn commits(&self) -> &[Commit<'_>; N] { + TryFrom::try_from(self.commits.as_slice()).unwrap() + } +} + +/// Creates 4 commits in a shared state space on top of the base hugr +/// `simple_hugr`: /// /// ``` /// ┌─────────┐ @@ -205,23 +240,23 @@ fn create_not_and_to_xor_replacement(hugr: &Hugr) -> SimpleReplacement { /// - `commit1` and `commit2` are disjoint with `commit4` (i.e. compatible), /// - `commit2` depends on `commit1` #[fixture] -pub(crate) fn test_state_space() -> (CommitStateSpace, [CommitId; 4]) { +pub(crate) fn test_state_space() -> TestStateSpace { let (base_hugr, [not0_node, not1_node, _and_node]) = simple_hugr(); - let mut state_space = CommitStateSpace::::with_base(base_hugr); + let state_space = CommitStateSpace::new(); + let base = state_space.try_set_base(base_hugr).unwrap(); // Create first replacement (replace NOT0 with two NOT gates) - let replacement1 = create_double_not_replacement(state_space.base_hugr(), not0_node); + let replacement1 = create_double_not_replacement(base.commit_hugr(), not0_node); // Add first commit to state space, replacing NOT0 with two NOT gates let commit1 = { - let to_patch_node = |n: Node| PatchNode(state_space.base(), n); - let new_host = state_space.try_extract_hugr([state_space.base()]).unwrap(); + let new_host = PersistentHugr::try_new([base.clone()]).unwrap(); // translate replacement1 to patch nodes in the base commit of the state space let replacement1 = replacement1 - .map_host_nodes(to_patch_node, &new_host) + .map_host_nodes(|n| base.to_patch_node(n), &new_host) .unwrap(); - state_space.try_add_replacement(replacement1).unwrap() + Commit::try_from_replacement(replacement1, &state_space).unwrap() }; // Add second commit to state space, that applies on top of `commit1` and @@ -229,7 +264,7 @@ pub(crate) fn test_state_space() -> (CommitStateSpace, [CommitId let commit2 = { // Create second replacement (replace NOT+AND with XOR) that applies on // the result of the first - let mut direct_hugr = state_space.base_hugr().clone(); + let mut direct_hugr = base.commit_hugr().clone(); let node_map = replacement1 .clone() .apply(&mut direct_hugr) @@ -247,51 +282,52 @@ pub(crate) fn test_state_space() -> (CommitStateSpace, [CommitId inv }; let to_patch_node = { - let base_commit = state_space.base(); - move |n| { + |n| { if let Some(&n) = inv_node_map.get(&n) { // node was replaced by commit1 - PatchNode(commit1, n) + commit1.to_patch_node(n) } else { // node is in base hugr - PatchNode(base_commit, n) + base.to_patch_node(n) } } }; // translate replacement2 to patch nodes - let new_host = state_space.try_extract_hugr([commit1]).unwrap(); + let new_host = PersistentHugr::try_new([commit1.clone()]).unwrap(); let replacement2 = replacement2 .map_host_nodes(to_patch_node, &new_host) .unwrap(); - state_space.try_add_replacement(replacement2).unwrap() + Commit::try_from_replacement(replacement2, &state_space).unwrap() }; // Create a third commit that will conflict with `commit1`, replacing NOT0 // and AND with XOR let commit3 = { - let replacement3 = create_not_and_to_xor_replacement(state_space.base_hugr()); - let to_patch_node = |n: Node| PatchNode(state_space.base(), n); - let new_host = state_space.try_extract_hugr([state_space.base()]).unwrap(); + let replacement3 = create_not_and_to_xor_replacement(base.commit_hugr()); + let new_host = PersistentHugr::try_new([commit1.clone()]).unwrap(); let replacement3 = replacement3 - .map_host_nodes(to_patch_node, &new_host) + .map_host_nodes(|n| base.to_patch_node(n), &new_host) .unwrap(); - state_space.try_add_replacement(replacement3).unwrap() + Commit::try_from_replacement(replacement3, &state_space).unwrap() }; // Create a fourth commit that is disjoint from `commit1`, replacing NOT1 // with two NOT gates let commit4 = { - let replacement4 = create_double_not_replacement(state_space.base_hugr(), not1_node); - let to_patch_node = |n: Node| PatchNode(state_space.base(), n); - let new_host = state_space.try_extract_hugr([state_space.base()]).unwrap(); + let replacement4 = create_double_not_replacement(base.commit_hugr(), not1_node); + let new_host = PersistentHugr::try_new([commit1.clone()]).unwrap(); let replacement4 = replacement4 - .map_host_nodes(to_patch_node, &new_host) + .map_host_nodes(|n| base.to_patch_node(n), &new_host) .unwrap(); - state_space.try_add_replacement(replacement4).unwrap() + Commit::try_from_replacement(replacement4, &state_space).unwrap() }; - (state_space, [commit1, commit2, commit3, commit4]) + // [commit1, commit2, commit3, commit4] + TestStateSpace::new( + state_space.clone(), + vec![commit1, commit2, commit3, commit4], + ) } #[fixture] @@ -333,8 +369,8 @@ pub(super) fn persistent_hugr_empty_child() -> (PersistentHugr, [CommitId; 2], [ } #[rstest] -fn test_successive_replacements(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, commit2, _commit3, _commit4]) = test_state_space; +fn test_successive_replacements(test_state_space: TestStateSpace) { + let [commit1, commit2, _commit3, _commit4] = test_state_space.commits(); let (mut hugr, [not0_node, _not1_node, _and_node]) = simple_hugr(); // Apply first replacement (replace NOT0 with two NOT gates) @@ -346,8 +382,7 @@ fn test_successive_replacements(test_state_space: (CommitStateSpace, [CommitId; replacement2.clone().apply(&mut hugr).unwrap(); // Create a persistent hugr - let persistent_hugr = state_space - .try_extract_hugr([commit1, commit2]) + let persistent_hugr = PersistentHugr::try_new([commit1.clone(), commit2.clone()]) .expect("commit1 and commit2 are compatible"); // Get the final hugr from the persistent context @@ -366,8 +401,9 @@ fn test_successive_replacements(test_state_space: (CommitStateSpace, [CommitId; } #[rstest] -fn test_conflicting_replacements(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, _commit2, commit3, _commit4]) = test_state_space; +fn test_conflicting_replacements(test_state_space: TestStateSpace) { + let [commit1, _commit2, commit3, _commit4] = test_state_space.commits(); + let state_space = commit1.state_space(); let (hugr, [not0_node, _not1_node, _and_node]) = simple_hugr(); // Apply first replacement directly to a clone @@ -387,35 +423,31 @@ fn test_conflicting_replacements(test_state_space: (CommitStateSpace, [CommitId; }; // Create a persistent hugr and add first replacement - let persistent_hugr1 = state_space.try_extract_hugr([commit1]).unwrap(); + let persistent_hugr1 = PersistentHugr::try_new([commit1.clone()]).unwrap(); // Create another persistent hugr and add second replacement - let persistent_hugr2 = state_space.try_extract_hugr([commit3]).unwrap(); + let persistent_hugr2 = PersistentHugr::try_new([commit3.clone()]).unwrap(); // Both individual replacements should be valid assert_eq!(persistent_hugr1.to_hugr().validate(), Ok(())); assert_eq!(persistent_hugr2.to_hugr().validate(), Ok(())); // But trying to create a history with both replacements should fail - let common_state_space = { - let mut space = persistent_hugr1.clone().into_state_space(); - space.extend(persistent_hugr2.clone()); - space - }; - assert_eq!(common_state_space.all_commit_ids().count(), 3); - let result = common_state_space.try_extract_hugr(common_state_space.all_commit_ids()); + let result = state_space.try_create( + persistent_hugr1 + .all_commit_ids() + .chain(persistent_hugr2.all_commit_ids()), + ); assert!( result.is_err(), "Creating history with conflicting patches should fail" ); - // TODO: use node-invariant equivalence check, e.g. hash-based comparison assert_eq!( hugr1.mermaid_string(), persistent_hugr1.to_hugr().mermaid_string() ); - // TODO: use node-invariant equivalence check, e.g. hash-based comparison assert_eq!( hugr2.mermaid_string(), persistent_hugr2.to_hugr().mermaid_string() @@ -423,8 +455,8 @@ fn test_conflicting_replacements(test_state_space: (CommitStateSpace, [CommitId; } #[rstest] -fn test_disjoint_replacements(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, _commit2, _commit3, commit4]) = test_state_space; +fn test_disjoint_replacements(test_state_space: TestStateSpace) { + let [commit1, _commit2, _commit3, commit4] = test_state_space.commits(); let (mut hugr, [not0_node, not1_node, _and_node]) = simple_hugr(); // Create and apply non-overlapping replacements for NOT0 and NOT1 @@ -434,7 +466,7 @@ fn test_disjoint_replacements(test_state_space: (CommitStateSpace, [CommitId; 4] replacement2.clone().apply(&mut hugr).unwrap(); // Create a persistent hugr and add both replacements - let persistent_hugr = state_space.try_extract_hugr([commit1, commit4]).unwrap(); + let persistent_hugr = PersistentHugr::try_new([commit1.clone(), commit4.clone()]).unwrap(); // Get the final hugr let persistent_final_hugr = persistent_hugr.to_hugr(); @@ -454,23 +486,22 @@ fn test_disjoint_replacements(test_state_space: (CommitStateSpace, [CommitId; 4] } #[rstest] -fn test_try_add_replacement(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, commit2, commit3, commit4]) = test_state_space; +fn test_try_add_replacement(test_state_space: TestStateSpace) { + let [commit1, commit2, commit3, commit4] = test_state_space.commits(); // Create a persistent hugr and add first replacement - let persistent_hugr = state_space.try_extract_hugr([commit1, commit2]).unwrap(); + let persistent_hugr = PersistentHugr::try_new([commit1.clone(), commit2.clone()]).unwrap(); { let mut persistent_hugr = persistent_hugr.clone(); - let repl4 = state_space.get_commit(commit4).replacement().unwrap(); + let repl4 = commit4.replacement().unwrap(); let result = persistent_hugr.try_add_replacement(repl4.clone()); assert!( result.is_ok(), "[commit1, commit2] + [commit4] are compatible. Got {result:?}" ); let hugr = persistent_hugr.to_hugr(); - let exp_hugr = state_space - .try_extract_hugr([commit1, commit2, commit4]) + let exp_hugr = PersistentHugr::try_new([commit1.clone(), commit2.clone(), commit4.clone()]) .unwrap() .to_hugr(); assert_eq!(hugr.mermaid_string(), exp_hugr.mermaid_string()); @@ -478,7 +509,7 @@ fn test_try_add_replacement(test_state_space: (CommitStateSpace, [CommitId; 4])) { let mut persistent_hugr = persistent_hugr.clone(); - let repl3 = state_space.get_commit(commit3).replacement().unwrap(); + let repl3 = commit3.replacement().unwrap(); let result = persistent_hugr.try_add_replacement(repl3.clone()); assert!( result.is_err(), @@ -489,33 +520,27 @@ fn test_try_add_replacement(test_state_space: (CommitStateSpace, [CommitId; 4])) // same test as above, but using try_add_commit instead of try_add_replacement #[rstest] -fn test_try_add_commit(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, commit2, commit3, commit4]) = test_state_space; +fn test_try_add_commit(test_state_space: TestStateSpace) { + let [commit1, commit2, commit3, commit4] = test_state_space.commits(); + let state_space = commit1.state_space(); // Create a persistent hugr and add first replacement - let persistent_hugr = state_space.try_extract_hugr([commit1, commit2]).unwrap(); + let persistent_hugr = PersistentHugr::try_new([commit1.clone(), commit2.clone()]).unwrap(); { let mut persistent_hugr = persistent_hugr.clone(); - let repl4 = state_space - .get_commit(commit4) - .replacement() - .unwrap() - .clone(); + let repl4 = commit4.replacement().unwrap().clone(); let new_commit = Commit::try_from_replacement(repl4, &state_space).unwrap(); - let commit4 = persistent_hugr + let new_commit4_id = persistent_hugr .try_add_commit(new_commit) .expect("commit4 is compatible"); + let new_commit4 = persistent_hugr.get_commit(new_commit4_id); - assert_eq!(persistent_hugr.inserted_nodes(commit4).count(), 2); + assert_eq!(new_commit4.inserted_nodes().count(), 2); } { let mut persistent_hugr = persistent_hugr.clone(); - let repl3 = state_space - .get_commit(commit3) - .replacement() - .unwrap() - .clone(); + let repl3 = commit3.replacement().unwrap().clone(); let new_commit = Commit::try_from_replacement(repl3, &state_space).unwrap(); persistent_hugr .try_add_commit(new_commit) diff --git a/hugr-persistent/src/trait_impls.rs b/hugr-persistent/src/trait_impls.rs index 2d7c04e16d..8dce6ec922 100644 --- a/hugr-persistent/src/trait_impls.rs +++ b/hugr-persistent/src/trait_impls.rs @@ -16,17 +16,19 @@ use hugr_core::{ ops::OpType, }; +use crate::CommitId; + use super::{ InvalidCommit, PatchNode, PersistentHugr, PersistentReplacement, state_space::CommitData, }; impl Patch for PersistentReplacement { - type Outcome = (); + type Outcome = CommitId; const UNCHANGED_ON_FAILURE: bool = true; fn apply(self, h: &mut PersistentHugr) -> Result { match h.try_add_replacement(self) { - Ok(_) => Ok(()), + Ok(commit) => Ok(commit), Err( InvalidCommit::UnknownParent(_) | InvalidCommit::IncompatibleHistory(_, _) @@ -37,7 +39,7 @@ impl Patch for PersistentReplacement { } } -impl HugrInternals for PersistentHugr { +impl HugrInternals for PersistentHugr { type RegionPortgraph<'p> = portgraph::MultiPortGraph where @@ -61,8 +63,9 @@ impl HugrInternals for PersistentHugr { (hugr.into_region_portgraph(parent), node_map) } - fn node_metadata_map(&self, node: Self::Node) -> &hugr::NodeMetadataMap { - self.as_state_space().node_metadata_map(node) + fn node_metadata_map(&self, PatchNode(commit_id, node): Self::Node) -> &hugr::NodeMetadataMap { + let cm = self.get_commit(commit_id); + cm.node_metadata_map(node) } } @@ -71,7 +74,7 @@ impl HugrInternals for PersistentHugr { // the whole extracted HUGR in memory. We are currently prioritizing correctness // and clarity over performance and will optimise some of these operations in // the future as bottlenecks are encountered. (see #2248) -impl HugrView for PersistentHugr { +impl HugrView for PersistentHugr { fn entrypoint(&self) -> Self::Node { // The entrypoint remains unchanged throughout the patch history, and is // found in the base hugr. @@ -107,15 +110,17 @@ impl HugrView for PersistentHugr { Some(parent_inv) } - fn get_optype(&self, node: Self::Node) -> &OpType { - self.as_state_space().get_optype(node) + fn get_optype(&self, PatchNode(commit_id, node): Self::Node) -> &OpType { + let cm = self.get_commit(commit_id); + cm.get_optype(node) } fn num_nodes(&self) -> usize { let mut num_nodes = 0isize; - for commit in self.all_commit_ids() { - num_nodes += self.inserted_nodes(commit).count() as isize; - num_nodes -= self.deleted_nodes(commit).count() as isize; + for id in self.all_commit_ids() { + let commit = self.get_commit(id); + num_nodes += commit.inserted_nodes().count() as isize; + num_nodes -= self.deleted_nodes(commit.id()).count() as isize; } num_nodes as usize } @@ -124,8 +129,9 @@ impl HugrView for PersistentHugr { self.to_hugr().num_edges() } - fn num_ports(&self, node: Self::Node, dir: Direction) -> usize { - self.as_state_space().num_ports(node, dir) + fn num_ports(&self, PatchNode(commit_id, node): Self::Node, dir: Direction) -> usize { + let cm = self.get_commit(commit_id); + cm.num_ports(node, dir) } fn nodes(&self) -> impl Iterator + Clone { @@ -148,38 +154,44 @@ impl HugrView for PersistentHugr { .filter(|&n| self.contains_node(n)) } - fn node_ports(&self, node: Self::Node, dir: Direction) -> impl Iterator + Clone { - self.as_state_space().node_ports(node, dir) + fn node_ports( + &self, + PatchNode(commit_id, node): Self::Node, + dir: Direction, + ) -> impl Iterator + Clone { + let cm = self.get_commit(commit_id); + cm.node_ports(node, dir) } - fn all_node_ports(&self, node: Self::Node) -> impl Iterator + Clone { - self.as_state_space().all_node_ports(node) + fn all_node_ports( + &self, + PatchNode(commit_id, node): Self::Node, + ) -> impl Iterator + Clone { + let cm = self.get_commit(commit_id); + cm.all_node_ports(node) } fn linked_ports( &self, - node: Self::Node, + PatchNode(commit_id, node): Self::Node, port: impl Into, ) -> impl Iterator + Clone { let port = port.into(); let mut ret_ports = Vec::new(); - if !self.is_value_port(node, port) { + let cm = self.get_commit(commit_id); + if !cm.is_value_port(node, port) { // currently non-value ports are not modified by patches - let commit_id = node.0; - let to_patch_node = |(node, port)| (PatchNode(commit_id, node), port); - ret_ports.extend( - self.commit_hugr(commit_id) - .linked_ports(node.1, port) - .map(to_patch_node), - ); + let to_patch_node = |(node, port)| (cm.to_patch_node(node), port); + ret_ports.extend(cm.commit_hugr().linked_ports(node, port).map(to_patch_node)); } else { match port.as_directed() { Either::Left(incoming) => { - let (out_node, out_port) = self.single_outgoing_port(node, incoming); + let (out_node, out_port) = + self.single_outgoing_port(cm.to_patch_node(node), incoming); ret_ports.push((out_node, out_port.into())) } Either::Right(outgoing) => ret_ports.extend( - self.all_incoming_ports(node, outgoing) + self.all_incoming_ports(cm.to_patch_node(node), outgoing) .map(|(node, port)| (node, port.into())), ), } @@ -321,7 +333,7 @@ impl HugrView for PersistentHugr { mod tests { use std::collections::HashSet; - use crate::{CommitStateSpace, state_space::CommitId}; + use crate::tests::TestStateSpace; use super::super::tests::test_state_space; use super::*; @@ -330,12 +342,11 @@ mod tests { use rstest::rstest; #[rstest] - fn test_mermaid_string(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, commit2, _commit3, commit4]) = test_state_space; + fn test_mermaid_string(test_state_space: TestStateSpace) { + let [commit1, commit2, _commit3, commit4] = test_state_space.commits(); - let hugr = state_space - .try_extract_hugr([commit1, commit2, commit4]) - .unwrap(); + let hugr = + PersistentHugr::try_new([commit1.clone(), commit2.clone(), commit4.clone()]).unwrap(); let mermaid_str = hugr .mermaid_format() @@ -351,15 +362,20 @@ mod tests { } #[rstest] - fn test_hierarchy(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, commit2, _commit3, commit4]) = test_state_space; + fn test_hierarchy(test_state_space: TestStateSpace) { + let [commit1, commit2, _commit3, commit4] = test_state_space.commits(); - let hugr = state_space - .try_extract_hugr([commit1, commit2, commit4]) - .unwrap(); + let hugr = + PersistentHugr::try_new([commit1.clone(), commit2.clone(), commit4.clone()]).unwrap(); - let commit2_nodes = hugr.nodes().filter(|&n| n.0 == commit2).collect_vec(); - let commit4_nodes = hugr.nodes().filter(|&n| n.0 == commit4).collect_vec(); + let commit2_nodes = hugr + .nodes() + .filter(|&n| n.owner() == commit2.id()) + .collect_vec(); + let commit4_nodes = hugr + .nodes() + .filter(|&n| n.owner() == commit4.id()) + .collect_vec(); let all_children: HashSet<_> = hugr.children(hugr.entrypoint()).collect(); @@ -385,12 +401,11 @@ mod tests { } #[rstest] - fn test_linked_ports(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, commit2, _commit3, commit4]) = test_state_space; + fn test_linked_ports(test_state_space: TestStateSpace) { + let [commit1, commit2, _commit3, commit4] = test_state_space.commits(); - let hugr = state_space - .try_extract_hugr([commit1, commit2, commit4]) - .unwrap(); + let hugr = + PersistentHugr::try_new([commit1.clone(), commit2.clone(), commit4.clone()]).unwrap(); let (extracted_hugr, node_map) = hugr.apply_all(); for n in hugr.nodes() { @@ -441,17 +456,17 @@ mod tests { } #[rstest] - fn test_extract_hugr(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, commit2, _commit3, commit4]) = test_state_space; + fn test_extract_hugr(test_state_space: TestStateSpace) { + let [commit1, commit2, _commit3, commit4] = test_state_space.commits(); - let hugr = state_space - .try_extract_hugr([commit1, commit2, commit4]) - .unwrap(); + let hugr = + PersistentHugr::try_new([commit1.clone(), commit2.clone(), commit4.clone()]).unwrap(); let extracted_hugr = hugr.to_hugr(); assert_eq!( hugr.module_root(), - PatchNode(state_space.base(), state_space.base_hugr().module_root()) + hugr.base_commit() + .to_patch_node(hugr.base_hugr().module_root()) ); assert_eq!(hugr.num_nodes(), extracted_hugr.num_nodes()); diff --git a/hugr-persistent/src/walker.rs b/hugr-persistent/src/walker.rs index bf579398c5..db376d235d 100644 --- a/hugr-persistent/src/walker.rs +++ b/hugr-persistent/src/walker.rs @@ -1,13 +1,12 @@ //! Incremental traversal and construction of [`PersistentHugr`]s from //! [`CommitStateSpace`]s. //! -//! This module provides the [`Walker`] type, which enables intuitive -//! exploration of a [`CommitStateSpace`] by traversing wires and gradually -//! pinning (selecting) nodes that match a desired pattern. Unlike direct -//! manipulation of a [`CommitStateSpace`], which may contain many alternative -//! and conflicting versions of the graph data, the Walker presents a familiar -//! nodes-and-wires interface that feels similar to walking a standard (HUGR) -//! graph. +//! This module provides the [`Walker`] type, which enables exploration of a +//! [`CommitStateSpace`] by traversing wires and gradually pinning (selecting) +//! nodes that match a desired pattern. Unlike direct manipulation of a +//! [`CommitStateSpace`], which may contain many alternative and conflicting +//! versions of the graph data, the Walker presents a familiar nodes-and-wires +//! interface that feels similar to walking a standard (HUGR) graph. //! //! The key concept is that of "pinning" nodes - marking specific nodes as fixed //! points in the exploration. As more nodes are pinned, the space of possible @@ -55,8 +54,9 @@ //! versions of the graph simultaneously, without having to materialize //! each version separately. -use std::{borrow::Cow, collections::BTreeSet}; +use std::collections::BTreeSet; +use hugr_core::Node; use hugr_core::hugr::patch::simple_replace::BoundaryMode; use hugr_core::ops::handle::DataflowParentID; use itertools::{Either, Itertools}; @@ -66,7 +66,7 @@ use hugr_core::{Direction, Hugr, HugrView, Port, PortIndex, hugr::views::RootChe use crate::{Commit, PersistentReplacement, PinnedSubgraph}; -use crate::{PersistentWire, PointerEqResolver, resolver::Resolver}; +use crate::PersistentWire; use super::{CommitStateSpace, InvalidCommit, PatchNode, PersistentHugr, state_space::CommitId}; @@ -88,30 +88,32 @@ use super::{CommitStateSpace, InvalidCommit, PatchNode, PersistentHugr, state_sp /// expansions of the current walker. /// current walker. #[derive(Debug, Clone)] -pub struct Walker<'a, R: Clone = PointerEqResolver> { +pub struct Walker<'a> { /// The state space being traversed. - state_space: Cow<'a, CommitStateSpace>, + state_space: &'a CommitStateSpace, /// The subset of compatible commits in `state_space` that are currently /// selected. // Note that we could store this as a set of `CommitId`s, but it is very // convenient to have access to all the methods of PersistentHugr (on top // of guaranteeing the compatibility invariant). The tradeoff is more // memory consumption. - selected_commits: PersistentHugr, + selected_commits: PersistentHugr, /// The set of nodes that have been traversed by the walker and can no /// longer be rewritten. pinned_nodes: BTreeSet, } -impl<'a, R: Resolver> Walker<'a, R> { +impl<'a> Walker<'a> { /// Create a new [`Walker`] over the given state space. /// /// No nodes are pinned initially. The [`Walker`] starts with only the base /// Hugr `state_space.base_hugr()` selected. - pub fn new(state_space: impl Into>>) -> Self { - let state_space = state_space.into(); - let base = state_space.base_commit().clone(); - let selected_commits: PersistentHugr = PersistentHugr::from_commit(base); + /// + /// # Panics + /// Panics if the commit state space is empty. + pub fn new(state_space: &'a CommitStateSpace) -> Self { + let base = state_space.base_commit().expect("non-empty state space"); + let selected_commits: PersistentHugr = PersistentHugr::from_commit(base); Self { state_space, selected_commits, @@ -120,10 +122,7 @@ impl<'a, R: Resolver> Walker<'a, R> { } /// Create a new [`Walker`] with a single pinned node. - pub fn from_pinned_node( - node: PatchNode, - state_space: impl Into>>, - ) -> Self { + pub fn from_pinned_node(node: PatchNode, state_space: &'a CommitStateSpace) -> Self { let mut walker = Self::new(state_space); walker .try_pin_node(node) @@ -149,7 +148,10 @@ impl<'a, R: Resolver> Walker<'a, R> { return Err(PinNodeError::AlreadyDeleted(node)); } } else { - let commit = self.state_space.get_commit(commit_id).clone(); + let commit = self + .state_space + .try_upgrade(commit_id) + .ok_or(PinNodeError::UnknownCommitId(commit_id))?; self.try_select_commit(commit)?; } Ok(self.pinned_nodes.insert(node)) @@ -216,7 +218,7 @@ impl<'a, R: Resolver> Walker<'a, R> { &'b self, wire: &'b PersistentWire, dir: impl Into>, - ) -> impl Iterator> + 'b { + ) -> impl Iterator> + 'b { let dir = dir.into(); if self.is_complete(wire, dir) { @@ -244,11 +246,20 @@ impl<'a, R: Resolver> Walker<'a, R> { "trying to pin already pinned node and no new commit is selected" ); + // Upgrade the commit IDs + let new_commits = new_commits + .iter() + .map(|&id| self.state_space.try_upgrade(id)) + .collect::>>()?; + // Update the selected commits to include the new commits. - let new_selected_commits = self - .state_space - .try_extract_hugr(self.selected_commits.all_commit_ids().chain(new_commits)) - .ok()?; + let new_selected_commits = { + let mut phugr = self.selected_commits.clone(); + phugr.try_add_commits(new_commits).ok()?; + phugr + }; + // .try_create(self.selected_commits.all_commit_ids().chain(new_commits)) + // .ok()?; // Make sure that the pinned nodes are still valid after including the new // selected commits. @@ -262,7 +273,7 @@ impl<'a, R: Resolver> Walker<'a, R> { // Construct a new walker and pin `pinnable_node`. let mut new_walker = Walker { - state_space: self.state_space.clone(), + state_space: self.state_space, selected_commits: new_selected_commits, pinned_nodes: self.pinned_nodes.clone(), }; @@ -293,12 +304,12 @@ impl<'a, R: Resolver> Walker<'a, R> { subgraph: impl Into, repl: impl RootCheckable, map_boundary: impl Fn(PatchNode, Port) -> Port, - ) -> Result { + ) -> Result, InvalidCommit> { let pinned_subgraph = subgraph.into(); let subgraph = pinned_subgraph.to_sibling_subgraph(self.as_hugr_view())?; let selected_commits = pinned_subgraph .selected_commits() - .map(|id| self.state_space.get_commit(id).clone()); + .map(|id| self.selected_commits.get_commit(id).clone()); let repl = { let mut repl = repl.try_into_checked().expect("replacement is not DFG"); @@ -327,11 +338,9 @@ impl<'a, R: Resolver> Walker<'a, R> { PersistentReplacement::try_new(subgraph, self.as_hugr_view(), repl.into_hugr())? }; - Commit::try_new(repl, selected_commits, &self.state_space) + Commit::try_new(repl, selected_commits, self.state_space) } -} -impl Walker<'_, R> { /// Get the wire connected to a specified port of a pinned node. /// /// # Panics @@ -343,7 +352,7 @@ impl Walker<'_, R> { /// Materialise the [`PersistentHugr`] containing all the compatible commits /// that have been selected during exploration. - pub fn into_persistent_hugr(self) -> PersistentHugr { + pub fn into_persistent_hugr(self) -> PersistentHugr { self.selected_commits } @@ -354,7 +363,7 @@ impl Walker<'_, R> { /// expansions of the walker, this is the HUGR corresponding to selecting /// as few commits as possible (i.e. all the commits that have been selected /// so far and no more). - pub fn as_hugr_view(&self) -> &PersistentHugr { + pub fn as_hugr_view(&self) -> &PersistentHugr { &self.selected_commits } @@ -384,20 +393,20 @@ impl Walker<'_, R> { let mut index = 0; while index < all_ports.len() { let (node, port, empty_commits) = all_ports[index].clone(); + let Some(commit) = self.state_space.try_upgrade(node.owner()) else { + continue; + }; index += 1; - for (child_id, (opp_node, opp_port)) in - self.state_space.children_at_boundary_port(node, port) + for (child, (opp_node, opp_port)) in + commit.children_at_boundary_port(node.1, port, self.state_space) { - for (node, port) in self.state_space.linked_child_ports( - opp_node, - opp_port, - child_id, - BoundaryMode::SnapToHost, - ) { + for (node, port) in + commit.linked_child_ports(opp_node, opp_port, &child, BoundaryMode::SnapToHost) + { let mut empty_commits = empty_commits.clone(); - if node.0 != child_id { - empty_commits.insert(child_id); + if node.owner() != child.id() { + empty_commits.insert(child.id()); } all_ports.push((node, port, empty_commits)); } @@ -408,11 +417,11 @@ impl Walker<'_, R> { } #[cfg(test)] -impl Walker<'_, R> { - // Check walker equality by comparing pointers to the state space and - // other fields. Only for testing purposes. +impl Walker<'_> { + // Check walker equality using pointer equality component-wise. For testing + // purposes. fn component_wise_ptr_eq(&self, other: &Self) -> bool { - std::ptr::eq(self.state_space.as_ref(), other.state_space.as_ref()) + self.state_space == other.state_space && self.pinned_nodes == other.pinned_nodes && BTreeSet::from_iter(self.selected_commits.all_commit_ids()) == BTreeSet::from_iter(other.selected_commits.all_commit_ids()) @@ -428,36 +437,33 @@ impl Walker<'_, R> { } } -impl CommitStateSpace { - /// Given a node and port, return all child commits of the current `node` - /// that delete `node` but keep at least one port linked to `(node, port)`. - /// In other words, (node, port) is a boundary port of the subgraph of - /// the child replacement. +impl<'a> Commit<'a> { + /// Given a node and port in `self`, return all child commits of `self` + /// in the state space that delete `node` but keep at least one port linked + /// to `(node, port)`. In other words, (node, port) is a boundary port + /// of the subgraph of the child replacement. /// - /// Return all tuples of children and linked port of (node, port) that is - /// outside of the subgraph of the child. The returned ports are opposite - /// to the direction of `port`. + /// Return all tuples of children and linked port of (node, port) in `self` + /// that is outside of the subgraph of the child. The returned ports are + /// opposite to the direction of `port`. fn children_at_boundary_port( &self, - patch_node @ PatchNode(commit_id, node): PatchNode, + node: Node, port: Port, - ) -> impl Iterator + '_ { - let linked_ports = self - .commit_hugr(commit_id) - .linked_ports(node, port) - .collect_vec(); + state_space: &'a CommitStateSpace, + ) -> impl Iterator, (Node, Port))> + '_ { + let linked_ports = self.commit_hugr().linked_ports(node, port).collect_vec(); - self.children(commit_id).flat_map(move |child_id| { - let deleted_nodes: BTreeSet<_> = self.get_commit(child_id).deleted_nodes().collect(); - if !deleted_nodes.contains(&patch_node) { + self.children(state_space).flat_map(move |child| { + let deleted_nodes: BTreeSet<_> = child.deleted_parent_nodes().collect(); + if !deleted_nodes.contains(&self.to_patch_node(node)) { vec![] } else { linked_ports .iter() .filter_map(move |&(linked_node, linked_port)| { - let linked_node = PatchNode(commit_id, linked_node); - (!deleted_nodes.contains(&linked_node)) - .then_some((child_id, (linked_node, linked_port))) + (!deleted_nodes.contains(&self.to_patch_node(linked_node))) + .then_some((child.clone(), (linked_node, linked_port))) }) .collect_vec() } @@ -479,6 +485,9 @@ pub enum PinNodeError { /// pinned. #[error("cannot delete already pinned node: {0}")] AlreadyPinned(PatchNode), + /// The commit ID is not in the state space or has been deleted. + #[error("unknown commit ID: {0:?}")] + UnknownCommitId(CommitId), } impl From for PinNodeError { @@ -487,15 +496,11 @@ impl From for PinNodeError { } } -impl<'a, R: Clone> From<&'a CommitStateSpace> for Cow<'a, CommitStateSpace> { - fn from(value: &'a CommitStateSpace) -> Self { - Cow::Borrowed(value) - } -} +impl<'w> hugr_core::hugr::views::NodesIter for Walker<'w> { + type Node = PatchNode; -impl From> for Cow<'_, CommitStateSpace> { - fn from(value: CommitStateSpace) -> Self { - Cow::Owned(value) + fn nodes(&self) -> impl Iterator + '_ { + ::nodes(self.as_hugr_view()) } } @@ -516,21 +521,23 @@ mod tests { use crate::{ PersistentHugr, Walker, state_space::CommitId, - tests::{persistent_hugr_empty_child, test_state_space}, + tests::{TestStateSpace, persistent_hugr_empty_child, test_state_space}, }; #[rstest] - fn test_walker_base_or_child_expansion(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, _commit2, _commit3, _commit4]) = test_state_space; + fn test_walker_base_or_child_expansion(test_state_space: TestStateSpace) { + let [commit1, _commit2, _commit3, _commit4] = test_state_space.commits(); + let state_space = commit1.state_space(); + let base_commit = commit1.base_commit(); // Get an initial node to pin let base_and_node = { - let base_hugr = state_space.base_hugr(); + let base_hugr = base_commit.commit_hugr(); let and_node = base_hugr .nodes() .find(|&n| base_hugr.get_optype(n) == &LogicOp::And.into()) .unwrap(); - PatchNode(state_space.base(), and_node) + base_commit.to_patch_node(and_node) }; let walker = Walker::from_pinned_node(base_and_node, &state_space); assert!(walker.is_pinned(base_and_node)); @@ -556,7 +563,7 @@ mod tests { assert!(new_walker.is_pinned(not_node)); // not node is either in commit1 or the base - assert!([commit1, state_space.base()].contains(¬_node.0)); + assert!([commit1.id(), base_commit.id()].contains(¬_node.0)); // not node is a NOT gate assert_eq!( @@ -565,7 +572,7 @@ mod tests { ); let persistent_hugr = new_walker.into_persistent_hugr(); - let hugr = persistent_hugr.commit_hugr(not_node.0); + let hugr = persistent_hugr.get_commit(not_node.owner()).commit_hugr(); assert_eq!(hugr.get_optype(not_node.1), &LogicOp::Not.into()); } } @@ -590,16 +597,18 @@ mod tests { /// commit2 /// ``` #[rstest] - fn test_walker_disjoint_nephew_expansion(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, commit2, commit3, commit4]) = test_state_space; + fn test_walker_disjoint_nephew_expansion(test_state_space: TestStateSpace) { + let [commit1, commit2, commit3, commit4] = test_state_space.commits(); + let base_commit = commit1.base_commit(); + let state_space = commit4.state_space(); // Initially, pin the second NOT of commit4 let not4_node = { - let repl4 = state_space.replacement(commit4).unwrap(); - let hugr4 = state_space.commit_hugr(commit4); + let repl4 = commit4.replacement().unwrap(); + let hugr4 = commit4.commit_hugr(); let [_, output] = repl4.get_replacement_io(); let (second_not_node, _) = hugr4.single_linked_output(output, 0).unwrap(); - PatchNode(commit4, second_not_node) + commit4.to_patch_node(second_not_node) }; let walker = Walker::from_pinned_node(not4_node, &state_space); assert!(walker.is_pinned(not4_node)); @@ -613,9 +622,9 @@ mod tests { // - XOR gate from commit3 // - XOR gate from commit2 (which implies commit1) let mut exp_options = BTreeSet::from_iter([ - BTreeSet::from_iter([state_space.base(), commit4]), - BTreeSet::from_iter([state_space.base(), commit3, commit4]), - BTreeSet::from_iter([state_space.base(), commit1, commit2, commit4]), + BTreeSet::from_iter([base_commit.id(), commit4.id()]), + BTreeSet::from_iter([base_commit.id(), commit3.id(), commit4.id()]), + BTreeSet::from_iter([base_commit.id(), commit1.id(), commit2.id(), commit4.id()]), ]); for new_walker in walker.expand(¬4_out, None) { // selected commits must be one of the valid options @@ -647,8 +656,8 @@ mod tests { // next_node is either in base (AND gate), in commit3 (XOR gate), or // in commit2 (XOR gate) let expected_optype = match next_node.0 { - commit_id if commit_id == state_space.base() => LogicOp::And, - commit_id if [commit2, commit3].contains(&commit_id) => LogicOp::Xor, + commit_id if commit_id == base_commit.id() => LogicOp::And, + commit_id if [commit2.id(), commit3.id()].contains(&commit_id) => LogicOp::Xor, _ => panic!("neighbour of not4 must be in base, commit2 or commit3"), }; assert_eq!( @@ -664,32 +673,33 @@ mod tests { } #[rstest] - fn test_get_wire_endpoints(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [commit1, commit2, _commit3, commit4]) = test_state_space; + fn test_get_wire_endpoints(test_state_space: TestStateSpace) { + let [commit1, commit2, _commit3, commit4] = test_state_space.commits(); + let base_commit = commit1.base_commit(); + let base_and_node = { - let base_hugr = state_space.base_hugr(); + let base_hugr = base_commit.commit_hugr(); let and_node = base_hugr .nodes() .find(|&n| base_hugr.get_optype(n) == &LogicOp::And.into()) .unwrap(); - PatchNode(state_space.base(), and_node) + base_commit.to_patch_node(and_node) }; - let hugr = state_space.try_extract_hugr([commit4]).unwrap(); + let hugr = PersistentHugr::try_new([commit4.clone()]).unwrap(); let (second_not_node, out_port) = hugr.single_outgoing_port(base_and_node, IncomingPort::from(1)); - assert_eq!(second_not_node.0, commit4); + assert_eq!(second_not_node.0, commit4.id()); assert_eq!(out_port, OutgoingPort::from(0)); - let hugr = state_space - .try_extract_hugr([commit1, commit2, commit4]) - .unwrap(); + let hugr = + PersistentHugr::try_new([commit1.clone(), commit2.clone(), commit4.clone()]).unwrap(); let (new_and_node, in_port) = hugr .all_incoming_ports(second_not_node, out_port) .exactly_one() .ok() .unwrap(); - assert_eq!(new_and_node.0, commit2); + assert_eq!(new_and_node.0, commit2.id()); assert_eq!(in_port, 1.into()); } @@ -703,7 +713,8 @@ mod tests { persistent_hugr_empty_child: (PersistentHugr, [CommitId; 2], [PatchNode; 3]), ) { let (hugr, [base_commit, empty_commit], [not0, not1, not2]) = persistent_hugr_empty_child; - let walker = Walker::from_pinned_node(not0, hugr.as_state_space()); + let state_space = hugr.state_space(); + let walker = Walker::from_pinned_node(not0, state_space); let not0_outwire = walker.get_wire(not0, OutgoingPort::from(0)); let expanded_wires = walker @@ -749,9 +760,11 @@ mod tests { fn test_create_commit_over_empty( persistent_hugr_empty_child: (PersistentHugr, [CommitId; 2], [PatchNode; 3]), ) { - let (hugr, [base_commit, empty_commit], [not0, _not1, not2]) = persistent_hugr_empty_child; + let (mut hugr, [base_commit, empty_commit], [not0, _not1, not2]) = + persistent_hugr_empty_child; + let state_space = hugr.state_space().clone(); let mut walker = Walker { - state_space: hugr.as_state_space().into(), + state_space: &state_space, selected_commits: hugr.clone(), pinned_nodes: BTreeSet::from_iter([not0]), }; @@ -782,10 +795,9 @@ mod tests { ) .unwrap(); - let mut new_state_space = hugr.as_state_space().to_owned(); - let commit_id = new_state_space.try_add_commit(commit.clone()).unwrap(); + let commit_id = hugr.try_add_commit(commit.clone()).unwrap(); assert_eq!( - new_state_space.parents(commit_id).collect::>(), + hugr.parent_commits(commit_id).collect::>(), BTreeSet::from_iter([base_commit, empty_commit]) ); @@ -811,7 +823,7 @@ mod tests { persistent_hugr_empty_child: (PersistentHugr, [CommitId; 2], [PatchNode; 3]), ) { let (hugr, [base_commit, empty_commit], [not0, _not1, not2]) = persistent_hugr_empty_child; - let mut walker = Walker::from_pinned_node(not0, hugr.as_state_space()); + let mut walker = Walker::from_pinned_node(not0, hugr.state_space()); assert!(walker.try_pin_node(not2).unwrap()); let not0_outwire = walker.get_wire(not0, OutgoingPort::from(0)); diff --git a/hugr-persistent/src/wire.rs b/hugr-persistent/src/wire.rs index a84d4e6923..eb2e45be07 100644 --- a/hugr-persistent/src/wire.rs +++ b/hugr-persistent/src/wire.rs @@ -6,7 +6,7 @@ use hugr_core::{ }; use itertools::Itertools; -use crate::{CommitId, PatchNode, PersistentHugr, Resolver, Walker}; +use crate::{CommitId, PatchNode, PersistentHugr, Walker}; /// A wire in a [`PersistentHugr`]. /// @@ -28,23 +28,23 @@ pub struct PersistentWire { struct CommitWire(Wire); impl CommitWire { - fn from_connected_port( + fn from_connected_port( PatchNode(commit_id, node): PatchNode, port: impl Into, - hugr: &PersistentHugr, + hugr: &PersistentHugr, ) -> Self { - let commit_hugr = hugr.commit_hugr(commit_id); + let commit_hugr = hugr.get_commit(commit_id).commit_hugr(); let wire = Wire::from_connected_port(node, port, commit_hugr); Self(Wire::new(PatchNode(commit_id, wire.node()), wire.source())) } - fn all_connected_ports<'h, R>( + fn all_connected_ports<'h>( &self, - hugr: &'h PersistentHugr, - ) -> impl Iterator + use<'h, R> { + hugr: &'h PersistentHugr, + ) -> impl Iterator + use<'h> { let wire = Wire::new(self.0.node().1, self.0.source()); let commit_id = self.commit_id(); - wire.all_connected_ports(hugr.commit_hugr(commit_id)) + wire.all_connected_ports(hugr.get_commit(commit_id).commit_hugr()) .map(move |(node, port)| (hugr.to_persistent_node(node, commit_id), port)) } @@ -74,7 +74,7 @@ enum NodeStatus { Valid, } -impl PersistentHugr { +impl PersistentHugr { pub fn get_wire(&self, node: PatchNode, port: impl Into) -> PersistentWire { PersistentWire::from_port(node, port, self) } @@ -84,7 +84,8 @@ impl PersistentHugr { fn node_status(&self, per_node @ PatchNode(commit_id, node): PatchNode) -> NodeStatus { debug_assert!(self.contains_id(commit_id), "unknown commit"); if self - .replacement(commit_id) + .get_commit(commit_id) + .replacement() .is_some_and(|repl| repl.get_replacement_io().contains(&node)) { NodeStatus::ReplacementIO @@ -94,11 +95,43 @@ impl PersistentHugr { NodeStatus::Valid } } + + /// The unique outgoing port in `self` that `port` is attached to. + /// + /// # Panics + /// + /// Panics if `node` is not in `self` (in particular if it is deleted) or if + /// `port` is not a value port in `node`. + pub(crate) fn single_outgoing_port( + &self, + node: PatchNode, + port: impl Into, + ) -> (PatchNode, OutgoingPort) { + let w = self.get_wire(node, port.into()); + w.single_outgoing_port(self) + .expect("found invalid dfg wire") + } + + /// All incoming ports that the given outgoing port is attached to. + /// + /// # Panics + /// + /// Panics if `out_node` is not in `self` (in particular if it is deleted) + /// or if `out_port` is not a value port in `out_node`. + pub(crate) fn all_incoming_ports( + &self, + out_node: PatchNode, + out_port: OutgoingPort, + ) -> impl Iterator { + let w = self.get_wire(out_node, out_port); + w.into_all_ports(self, Direction::Incoming) + .map(|(node, port)| (node, port.as_incoming().unwrap())) + } } impl PersistentWire { /// Get the wire connected to a specified port of a pinned node in `hugr`. - fn from_port(node: PatchNode, port: impl Into, per_hugr: &PersistentHugr) -> Self { + fn from_port(node: PatchNode, port: impl Into, per_hugr: &PersistentHugr) -> Self { assert!(per_hugr.contains_node(node), "node not in hugr"); // Queue of wires within each commit HUGR, that combined will form the @@ -109,7 +142,8 @@ impl PersistentWire { while let Some(wire) = queue.pop_front() { let commit_id = wire.commit_id(); - let commit_hugr = per_hugr.commit_hugr(commit_id); + let commit = per_hugr.get_commit(commit_id); + let commit_hugr = commit.commit_hugr(); let all_ports = wire.all_connected_ports(per_hugr); for (per_node @ PatchNode(_, node), port) in all_ports { @@ -119,15 +153,12 @@ impl PersistentWire { // ports on the opposite end of the wire and boundary // ports in the child commit that deleted the node. for (opp_node, opp_port) in commit_hugr.linked_ports(node, port) { - let opp_node = per_hugr.to_persistent_node(opp_node, commit_id); - for (child_node, child_port) in - per_hugr.as_state_space().linked_child_ports( - opp_node, - opp_port, - deleted_by, - BoundaryMode::IncludeIO, - ) - { + for (child_node, child_port) in commit.linked_child_ports( + opp_node, + opp_port, + per_hugr.get_commit(deleted_by), + BoundaryMode::IncludeIO, + ) { debug_assert_eq!(child_node.owner(), deleted_by); let w = CommitWire::from_connected_port( child_node, child_port, per_hugr, @@ -144,10 +175,8 @@ impl PersistentWire { // boundary ports of the commit (i.e. the ports connected to // the input resp. output) and ports in a parent commit. for (opp_node, opp_port) in commit_hugr.linked_ports(node, port) { - let opp_node = per_hugr.to_persistent_node(opp_node, commit_id); - for (parent_node, parent_port) in per_hugr - .as_state_space() - .linked_parent_ports(opp_node, opp_port) + for (parent_node, parent_port) in + commit.linked_parent_ports(opp_node, opp_port) { let w = CommitWire::from_connected_port( parent_node, @@ -173,9 +202,9 @@ impl PersistentWire { /// Get all ports attached to a wire in `hugr`. /// /// All ports returned are on nodes that are contained in `hugr`. - pub fn all_ports( + pub fn all_ports( &self, - hugr: &PersistentHugr, + hugr: &PersistentHugr, dir: impl Into>, ) -> impl Iterator { all_ports_impl(self.wires.iter().copied(), dir.into(), hugr) @@ -189,31 +218,28 @@ impl PersistentWire { /// Consume the wire and return all ports attached to a wire in `hugr`. /// /// All ports returned are on nodes that are contained in `hugr`. - pub fn into_all_ports( + pub fn into_all_ports( self, - hugr: &PersistentHugr, + hugr: &PersistentHugr, dir: impl Into>, ) -> impl Iterator { all_ports_impl(self.wires.into_iter(), dir.into(), hugr) } - pub fn single_outgoing_port( - &self, - hugr: &PersistentHugr, - ) -> Option<(PatchNode, OutgoingPort)> { + pub fn single_outgoing_port(&self, hugr: &PersistentHugr) -> Option<(PatchNode, OutgoingPort)> { single_outgoing(self.all_ports(hugr, Direction::Outgoing)) } - pub fn all_incoming_ports( + pub fn all_incoming_ports( &self, - hugr: &PersistentHugr, + hugr: &PersistentHugr, ) -> impl Iterator { self.all_ports(hugr, Direction::Incoming) .map(|(node, port)| (node, port.as_incoming().unwrap())) } } -impl Walker<'_, R> { +impl Walker<'_> { /// Get all ports on a wire that are not pinned in `self`. pub(crate) fn wire_unpinned_ports( &self, @@ -257,10 +283,10 @@ impl Walker<'_, R> { /// Implementation of the (shared) body of [`PersistentWire::all_ports`] and /// [`PersistentWire::into_all_ports`]. -fn all_ports_impl( +fn all_ports_impl( wires: impl Iterator, dir: Option, - per_hugr: &PersistentHugr, + per_hugr: &PersistentHugr, ) -> impl Iterator { let all_ports = wires.flat_map(move |w| w.all_connected_ports(per_hugr)); @@ -279,25 +305,28 @@ fn single_outgoing(iter: impl Iterator) -> Option<(N, Outgo mod tests { use std::collections::BTreeSet; - use crate::{CommitId, CommitStateSpace, PatchNode, tests::test_state_space}; + use crate::{ + PatchNode, PersistentHugr, + tests::{TestStateSpace, test_state_space}, + }; use hugr_core::{HugrView, OutgoingPort}; use itertools::Itertools; use rstest::rstest; #[rstest] - fn test_all_ports(test_state_space: (CommitStateSpace, [CommitId; 4])) { - let (state_space, [_, _, cm3, cm4]) = test_state_space; - let hugr = state_space.try_extract_hugr([cm3, cm4]).unwrap(); + fn test_all_ports(test_state_space: TestStateSpace) { + let [_, _, cm3, cm4] = test_state_space.commits(); + let hugr = PersistentHugr::try_new([cm3.clone(), cm4.clone()]).unwrap(); let cm4_not = { - let hugr4 = state_space.commit_hugr(cm4); - let out = state_space.replacement(cm4).unwrap().get_replacement_io()[1]; + let hugr4 = cm4.commit_hugr(); + let out = cm4.replacement().unwrap().get_replacement_io()[1]; let node = hugr4.input_neighbours(out).exactly_one().ok().unwrap(); - PatchNode(cm4, node) + PatchNode(cm4.id(), node) }; let w = hugr.get_wire(cm4_not, OutgoingPort::from(0)); assert_eq!( BTreeSet::from_iter(w.wires.iter().map(|w| w.0.node().0)), - BTreeSet::from_iter([cm3, cm4, state_space.base(),]) + BTreeSet::from_iter([cm3.id(), cm4.id(), hugr.base(),]) ); } } diff --git a/hugr-persistent/tests/persistent_walker_example.rs b/hugr-persistent/tests/persistent_walker_example.rs index 48fa0cbcb5..8499700b58 100644 --- a/hugr-persistent/tests/persistent_walker_example.rs +++ b/hugr-persistent/tests/persistent_walker_example.rs @@ -12,7 +12,9 @@ use hugr_core::{ types::EdgeKind, }; -use hugr_persistent::{Commit, CommitStateSpace, PersistentWire, PinnedSubgraph, Walker}; +use hugr_persistent::{ + Commit, CommitStateSpace, PersistentHugr, PersistentWire, PinnedSubgraph, Walker, +}; /// The maximum commit depth that we will consider in this example const MAX_COMMITS: usize = 4; @@ -109,41 +111,44 @@ fn two_cz_3qb_hugr() -> Hugr { /// Traverse all commits in state space, enqueueing all outgoing wires of /// CZ nodes -fn enqueue_all( - queue: &mut VecDeque<(PersistentWire, Walker<'static>)>, - state_space: &CommitStateSpace, +fn enqueue_all<'a>( + queue: &mut VecDeque<(PersistentWire, Walker<'a>)>, + all_commits: &[Commit], + state_space: &'a CommitStateSpace, ) { - for id in state_space.all_commit_ids() { - let cz_nodes = state_space - .inserted_nodes(id) - .filter(|&n| state_space.get_optype(n) == &cz_gate().into()); + for commit in all_commits { + let cz_nodes = commit + .inserted_nodes() + .filter(|&n| commit.get_optype(n) == &cz_gate().into()); for node in cz_nodes { - let walker: Walker<'static> = Walker::from_pinned_node(node, state_space.clone()); + let walker = Walker::from_pinned_node(commit.to_patch_node(node), state_space); if walker.as_hugr_view().all_commit_ids().count() > MAX_COMMITS { continue; } - for outport in state_space.node_outputs(node) { + for outport in commit.node_outputs(node) { if !matches!( - state_space.get_optype(node).port_kind(outport), + commit.get_optype(node).port_kind(outport), Some(EdgeKind::Value(_)) ) { continue; } - let wire = walker.get_wire(node, outport); + let wire = walker.get_wire(commit.to_patch_node(node), outport); queue.push_back((wire, walker.clone())); } } } } -fn build_state_space() -> CommitStateSpace { - let base_hugr = dfg_hugr(); - let mut state_space = CommitStateSpace::with_base(base_hugr); +fn explore_state_space<'a>( + base_commit: Commit<'a>, + state_space: &'a CommitStateSpace, +) -> Vec> { + let mut all_commits = vec![base_commit.clone()]; let mut wire_queue = VecDeque::new(); let mut added_patches = BTreeSet::new(); - enqueue_all(&mut wire_queue, &state_space); + enqueue_all(&mut wire_queue, &all_commits, state_space); while let Some((wire, walker)) = wire_queue.pop_front() { if !walker.is_complete(&wire, None) { @@ -174,7 +179,7 @@ fn build_state_space() -> CommitStateSpace { // check that the patch applies to more than one commit (or the base), // otherwise we have infinite commutations back and forth let patch_owners: BTreeSet<_> = patch_nodes.iter().map(|n| n.0).collect(); - if patch_owners.len() <= 1 && !patch_owners.contains(&state_space.base()) { + if patch_owners.len() <= 1 && !patch_owners.contains(&base_commit.id()) { continue; } // check further that the same patch was not already added to `state_space` @@ -188,23 +193,23 @@ fn build_state_space() -> CommitStateSpace { }; assert_eq!( - new_commit.deleted_nodes().collect::>(), + new_commit.deleted_parent_nodes().collect::>(), patch_nodes ); - state_space.try_add_commit(new_commit).unwrap(); + all_commits.push(new_commit); // enqueue new wires added by the replacement // (this will also add a lot of already visited wires, but they will // be deduplicated) - enqueue_all(&mut wire_queue, &state_space); + enqueue_all(&mut wire_queue, &all_commits, state_space); } } - state_space + all_commits } -fn create_commit(wire: PersistentWire, walker: &Walker) -> Option { +fn create_commit<'a>(wire: PersistentWire, walker: &Walker<'a>) -> Option> { let hugr = walker.clone().into_persistent_hugr(); let (out_node, _) = wire .single_outgoing_port(&hugr) @@ -312,40 +317,38 @@ fn create_commit(wire: PersistentWire, walker: &Walker) -> Option { #[ignore = "takes 10s (todo: optimise)"] #[test] fn walker_example() { - let state_space = build_state_space(); - println!("n commits = {:?}", state_space.all_commit_ids().count()); + let base_hugr = dfg_hugr(); + let state_space = CommitStateSpace::new(); + let base_commit = state_space.try_set_base(base_hugr).unwrap(); - for commit_id in state_space.all_commit_ids() { - println!("========== Commit {commit_id:?} ============"); + let all_commits = explore_state_space(base_commit, &state_space); + println!("n commits = {:?}", all_commits.len()); + + for commit in all_commits.iter() { + println!("========== Commit {:?} ============", commit.id()); println!( "parents = {:?}", - state_space.parents(commit_id).collect_vec() + commit.parents().map(|p| p.id()).collect_vec() ); println!( - "nodes deleted = {:?}", - state_space - .get_commit(commit_id) - .deleted_nodes() - .collect_vec() + "nodes deleted in parents = {:?}", + commit.deleted_parent_nodes().collect_vec() ); println!("nodes added:"); - println!( - "{:?}\n", - state_space.inserted_nodes(commit_id).collect_vec() - ); + println!("{:?}\n", commit.inserted_nodes().collect_vec()); } - let empty_commits = state_space - .all_commit_ids() - .filter(|&id| state_space.inserted_nodes(id).count() == 0) + let empty_commits = all_commits + .iter() + .filter(|cm| cm.inserted_nodes().count() == 0) .collect_vec(); // there should be a combination of three empty commits that are compatible // and such that the resulting HUGR is empty let mut empty_hugr = None; for cs in empty_commits.iter().combinations(3) { - let cs = cs.into_iter().copied(); - if let Ok(hugr) = state_space.try_extract_hugr(cs) { + let cs = cs.into_iter().copied().cloned(); + if let Ok(hugr) = PersistentHugr::try_new(cs) { empty_hugr = Some(hugr); } }