Skip to content

Commit 5780cda

Browse files
committed
feat: Add PersistentHugr
1 parent eda21fa commit 5780cda

File tree

6 files changed

+581
-0
lines changed

6 files changed

+581
-0
lines changed

hugr-core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ thiserror = { workspace = true }
5757
typetag = { workspace = true }
5858
semver = { workspace = true, features = ["serde"] }
5959
zstd = { workspace = true, optional = true }
60+
relrc = "0.4.0"
6061

6162
[dev-dependencies]
6263
rstest = { workspace = true }

hugr-core/src/hugr.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod hugrmut;
55
pub(crate) mod ident;
66
pub mod internal;
77
pub mod patch;
8+
pub mod persistent;
89
pub mod serialize;
910
pub mod validate;
1011
pub mod views;

hugr-core/src/hugr/persistent.rs

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
//! Persistent data structure for Hugr mutations.
2+
//!
3+
//! This module provides a persistent data structure [`PersistentHugr`] that
4+
//! implements [`crate::HugrView`]; mutations to the data are stored as patches
5+
//! in an append-only context ([`PersistentContext`]) that can be shared among
6+
//! multiple [`PersistentHugr`]s. As a result, all versions of the data and
7+
//! references to them remain valid even as new patches are applied.
8+
//!
9+
//! The dependencies between the set of patches that define a [`PersistentHugr`]
10+
//! form a directed acyclic graph called [`PatchHistory`]. A [`PatchHistory`] is
11+
//! an induced subgraph of the [`PatchGraph`] that is guaranteed to only contain
12+
//! non-overlapping patches. By applying all patches in a [`PatchHistory`], we
13+
//! can materialize a [`Hugr`]. Traversing the materialised Hugr is equivalent
14+
//! to using the [`crate::HugrView`] implementation of the corresponding
15+
//! [`PersistentHugr`].
16+
//!
17+
//! ## Summary of data types
18+
//!
19+
//! - [`PersistentHugr`] A persistent data structure that implements
20+
//! [`crate::HugrView`] and can be used as a drop-in replacement for a
21+
//! [`crate::Hugr`] for read-only access and mutations through patches.
22+
//! - [`PersistentContext`] Stores all possible mutations (patches) applied to
23+
//! [`PersistentHugr`] instances within a shared context. New
24+
//! [`PersistentHugr`] can thus be created that combine patches from multiple
25+
//! [`PersistentHugr`]s. It is a shared, reference-counted wrapper around a
26+
//! [`PatchGraph`].
27+
//! - [`PatchGraph`] An directed acyclic graph of patches, recording the
28+
//! dependencies between them. Includes the base Hugr and any number of
29+
//! possibly incompatible (overlapping) patches.
30+
//! - [`PatchHistory`] A compatible subset of patches from a [`PatchGraph`] that
31+
//! can be materialized into a [`Hugr`]. Guarantees that all included patches
32+
//! have their dependencies satisfied and do not conflict. Any
33+
//! [`PersistentHugr`] instance corresponds to a valid [`PatchHistory`].
34+
//!
35+
//! ## Usage
36+
//!
37+
//! A shared context can be created from a [`PatchGraph`]. Obtain a patch graph
38+
//! from a base hugr using [`PatchGraph::with_base`].
39+
//!
40+
//! A [`PersistentHugr`] can then be created with
41+
//! [`PersistentHugr::from_context_base`], or if you already have a set of patch
42+
//! IDs in the context, with [`PersistentHugr::try_from_patch_ids`]. Then add
43+
//! patches via [`PersistentHugr::add_replacement`], or apply them via
44+
//! patch traits.
45+
//!
46+
//! ## PersistentHugr as a "lens"
47+
//!
48+
//! A [`PersistentHugr`] is a view on a compatible subset of patches stored in a
49+
//! [`PersistentContext`]; the viewed data can also be modified by applying
50+
//! patches to it. This translates into adding patches to the context. In
51+
//! Haskell terminology, it is thus a "lens".
52+
53+
mod patch_graph;
54+
mod resolver;
55+
mod trait_impls;
56+
57+
use std::{
58+
cell::{Ref, RefCell},
59+
rc::Rc,
60+
};
61+
62+
pub use patch_graph::{
63+
IncompatibleHistory, InvalidPatch, PatchGraph, PatchHistory, PatchId, PatchNode,
64+
};
65+
pub use resolver::PointerEqResolver;
66+
67+
use crate::{Hugr, SimpleReplacement};
68+
69+
/// A replacement operation that can be applied to a [`PersistentHugr`].
70+
pub type PersistentReplacement = SimpleReplacement<PatchNode>;
71+
72+
/// A shared mutable context for [`PersistentHugr`], so that all possible
73+
/// patches are stored in one place.
74+
///
75+
/// [`PersistentHugr`]s that share a context will be adding all patches to a
76+
/// common [`PatchGraph`].
77+
///
78+
/// Construct a [`PersistentContext`] from a [`PatchGraph`] with [`From::from`].
79+
///
80+
/// Mutations to [`PersistentContext`] only ever add new patches, so it is sound
81+
/// to share the same copy across multiple [`PersistentHugr`]: no data is ever
82+
/// deleted and so no NodeIds will ever be invalidated.
83+
#[derive(Clone)]
84+
pub struct PersistentContext(Rc<RefCell<PatchGraph>>);
85+
86+
impl From<PatchGraph> for PersistentContext {
87+
fn from(graph: PatchGraph) -> Self {
88+
Self(Rc::new(RefCell::new(graph)))
89+
}
90+
}
91+
92+
impl PersistentContext {
93+
/// Get a shared reference to the underlying [`PatchGraph`].
94+
///
95+
/// ### Panics
96+
/// This will panic if a mutable borrow to the context exists.
97+
///
98+
/// Similarly, trying to mutably borrow whilst this reference exists, such
99+
/// as adding patches to any [`PersistentHugr`] with this context, will
100+
/// result in a panic.
101+
pub fn patch_graph_ref(&self) -> Ref<PatchGraph> {
102+
self.0.borrow()
103+
}
104+
}
105+
106+
/// A Hugr-like object that supports persistent mutation.
107+
///
108+
/// When mutations are applied to a [`PersistentHugr`], the object is mutated
109+
/// as expected but all references to previous versions of the object remain
110+
/// valid. Furthermore, older versions of the data can be recovered by
111+
/// traversing the object's history with [`Self::history`].
112+
///
113+
/// Multiple references to various versions of a Hugr can be maintained in
114+
/// parallel by sharing a single [`PersistentContext`] between them.
115+
///
116+
/// ## Supported mutation
117+
///
118+
/// [`PersistentHugr`] implements [`crate::HugrView`], so that it can used as
119+
/// a drop-in substitute for a Hugr wherever read-only access is required. It
120+
/// does not implement [`crate::hugr::HugrMut`], however. Mutations must be
121+
/// performed by applying patches (see [`crate::hugr::patch::VerifyPatch`] and
122+
/// [`crate::hugr::patch::ApplyPatch`]). Currently, only [`SimpleReplacement`]
123+
/// patches are supported. You can use [`Self::add_replacement`] to add a patch
124+
/// to `self`, or use the aforementioned patch traits.
125+
///
126+
/// ## Patches and history
127+
///
128+
/// A [`PersistentHugr`] is composed of a unique base Hug, along with a set of
129+
/// all mutations applied to it. All mutations are stored in the form of patches
130+
/// that apply on top of the base Hugr. You may think of it as a "queue" of
131+
/// patches: whenever a patch is "applied", it is in reality just added to the
132+
/// queue. In practice, the total order of the queue is irrelevant, as patches
133+
/// only depend on a subset of the previously applied patches. This creates a
134+
/// partial order on the patches---a directed acyclic graph that we call the
135+
/// patch history. Multiple [`PersistentHugr`] that share a common context then
136+
/// share a subset of that history.
137+
///
138+
/// The history of all patches applied and their dependencies is stored in a
139+
/// [`PatchHistory`] that can be retrieved with [`Self::history`].
140+
pub struct PersistentHugr {
141+
/// The context to which all patches are added.
142+
context: PersistentContext,
143+
/// The subset of patches stored in context that defines the Hugr.
144+
///
145+
/// This consists only of compatible patches, so that all patches can be
146+
/// applied. The resulting Hugr can be extracted with [`Self::to_hugr`].
147+
history: PatchHistory,
148+
}
149+
150+
impl PersistentHugr {
151+
/// Create a [`PersistentHugr`] corresponding to the base Hugr of the
152+
/// given context.
153+
pub fn from_context_base(context: PersistentContext) -> Self {
154+
let history = context.patch_graph_ref().base_history();
155+
Self { context, history }
156+
}
157+
158+
/// Create a [`PersistentHugr`] from a list of patch ids.
159+
///
160+
/// `Self` will correspond to the Hugr obtained by applying the history
161+
/// of all patches with the given IDs (and their ancestors).
162+
///
163+
/// `patch_ids` must be patches in `context`.
164+
///
165+
/// If the history of the patch ids would include two patches which are
166+
/// incompatible, an error is returned.
167+
pub fn try_from_patch_ids(
168+
patch_ids: impl IntoIterator<Item = PatchId>,
169+
context: PersistentContext,
170+
) -> Result<Self, IncompatibleHistory> {
171+
let history = context.patch_graph_ref().try_get_history(patch_ids)?;
172+
Ok(Self { context, history })
173+
}
174+
175+
/// Convert this `PersistentHugr` to a materialized Hugr by applying all
176+
/// patches in the current history.
177+
///
178+
/// This operation may be expensive and should be avoided in
179+
/// performance-critical paths. For read-only views into the data, rely
180+
/// instead on the [`crate::HugrView`] implementation when possible.
181+
pub fn to_hugr(&self) -> Hugr {
182+
self.history.to_hugr(&self.context.patch_graph_ref())
183+
}
184+
185+
/// The history of all patches in `self`.
186+
pub fn history(&self) -> &PatchHistory {
187+
&self.history
188+
}
189+
190+
/// Add a patch to `self`.
191+
///
192+
/// Currently, patches must be [`SimpleReplacement`]s.
193+
pub fn add_replacement(&mut self, replacement: PersistentReplacement) {
194+
let mut patch_graph = self.context.0.borrow_mut();
195+
let patch_id = patch_graph.add_replacement(replacement);
196+
self.history
197+
.try_add_patch(patch_id, &patch_graph)
198+
.expect("invalid nodes in replacement")
199+
}
200+
201+
/// Iterator over the patch IDs in the history.
202+
///
203+
/// The patches are not guaranteed to be in any particular order.
204+
pub fn patch_ids(&self) -> impl Iterator<Item = PatchId> + '_ {
205+
self.history.patch_ids()
206+
}
207+
}

0 commit comments

Comments
 (0)