Skip to content

Commit 718bae3

Browse files
authored
feat(persistent): Add serialisation for CommitStateSpace (#2344)
This PR adds serialisation for the `CommitStateSpace` type, following the pattern of #2300. To make the serialisation/deserialisation deterministic, a new `Resolver` is introduced. Resolvers define a notion of equivalence between commits in the state space. The simplest resolver is `PointerEqResolver`, which considers commits to be equivalent if they point to the same data in memory. The "key" to that commit is the memory pointer, and so is non-deterministic. The new resolver (`SerdeHashResolver`) determines commit equivalence by computing a hash of the serialised data. This is not very efficient and will have to be improved in the future, but it is correct and convenient to use for the time being. For this purpose, a fast, reproducible and platform-independent hash is used (`wyhash`). To accomodate multiple resolves, `CommitStateSpace` and `PersistentHugr` had to be made generic over the resolver type. Closes #2299
1 parent ebfc98e commit 718bae3

15 files changed

+807
-138
lines changed

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hugr-persistent/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@ license.workspace = true
1111
name = "persistent_walker_example"
1212

1313
[dependencies]
14-
relrc = { workspace = true, features = ["petgraph"] }
1514
hugr-core.path = "../hugr-core"
15+
1616
delegate.workspace = true
1717
derive_more.workspace = true
1818
itertools.workspace = true
1919
petgraph.workspace = true
2020
portgraph.workspace = true
21+
relrc = { workspace = true, features = ["petgraph", "serde"] }
22+
serde.workspace = true
23+
serde_json.workspace = true
2124
thiserror.workspace = true
25+
wyhash = "0.6.0"
2226

2327
[lints]
2428
workspace = true
@@ -27,3 +31,5 @@ workspace = true
2731
rstest.workspace = true
2832
lazy_static.workspace = true
2933
semver.workspace = true
34+
serde_with.workspace = true
35+
insta.workspace = true

hugr-persistent/src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ mod trait_impls;
7272
pub mod walker;
7373

7474
pub use persistent_hugr::{Commit, PersistentHugr};
75-
pub use resolver::PointerEqResolver;
75+
pub use resolver::{PointerEqResolver, Resolver, SerdeHashResolver};
7676
pub use state_space::{CommitId, CommitStateSpace, InvalidCommit, PatchNode};
7777
pub use walker::{PinnedWire, Walker};
7878

@@ -82,5 +82,11 @@ pub type PersistentReplacement = hugr_core::SimpleReplacement<PatchNode>;
8282
use persistent_hugr::find_conflicting_node;
8383
use state_space::CommitData;
8484

85+
pub mod serial {
86+
//! Serialized formats for commits, state spaces and persistent HUGRs.
87+
pub use super::persistent_hugr::serial::*;
88+
pub use super::state_space::serial::*;
89+
}
90+
8591
#[cfg(test)]
8692
mod tests;

hugr-persistent/src/parents_view.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ pub(crate) struct ParentsView<'a> {
2323
}
2424

2525
impl<'a> ParentsView<'a> {
26-
pub(crate) fn from_commit(commit_id: CommitId, state_space: &'a CommitStateSpace) -> Self {
26+
pub(crate) fn from_commit<R>(
27+
commit_id: CommitId,
28+
state_space: &'a CommitStateSpace<R>,
29+
) -> Self {
2730
let mut hugrs = BTreeMap::new();
2831
for parent in state_space.parents(commit_id) {
2932
hugrs.insert(parent, state_space.commit_hugr(parent));

hugr-persistent/src/persistent_hugr.rs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ use relrc::RelRc;
1414

1515
use crate::{
1616
CommitData, CommitId, CommitStateSpace, InvalidCommit, PatchNode, PersistentReplacement,
17+
Resolver,
1718
};
1819

20+
pub mod serial;
21+
1922
/// A patch that can be applied to a [`PersistentHugr`] or a
2023
/// [`CommitStateSpace`] as an atomic commit.
2124
///
@@ -41,9 +44,9 @@ impl Commit {
4144
/// If any of the parents of the replacement are not in the commit state
4245
/// space, this function will return an [`InvalidCommit::UnknownParent`]
4346
/// error.
44-
pub fn try_from_replacement(
47+
pub fn try_from_replacement<R>(
4548
replacement: PersistentReplacement,
46-
graph: &CommitStateSpace,
49+
graph: &CommitStateSpace<R>,
4750
) -> Result<Commit, InvalidCommit> {
4851
if replacement.subgraph().nodes().is_empty() {
4952
return Err(InvalidCommit::EmptyReplacement);
@@ -190,15 +193,15 @@ impl<'a> From<&'a RelRc<CommitData, ()>> for &'a Commit {
190193
/// Currently, only patches that apply to subgraphs within dataflow regions
191194
/// are supported.
192195
#[derive(Clone, Debug)]
193-
pub struct PersistentHugr {
196+
pub struct PersistentHugr<R = crate::PointerEqResolver> {
194197
/// The state space of all commits.
195198
///
196199
/// Invariant: all commits are "compatible", meaning that no two patches
197200
/// invalidate the same node.
198-
state_space: CommitStateSpace,
201+
state_space: CommitStateSpace<R>,
199202
}
200203

201-
impl PersistentHugr {
204+
impl<R: Resolver> PersistentHugr<R> {
202205
/// Create a [`PersistentHugr`] with `hugr` as its base HUGR.
203206
///
204207
/// All replacements added in the future will apply on top of `hugr`.
@@ -228,13 +231,6 @@ impl PersistentHugr {
228231
graph.try_extract_hugr(graph.all_commit_ids())
229232
}
230233

231-
/// Construct a [`PersistentHugr`] from a [`CommitStateSpace`].
232-
///
233-
/// Does not check that the commits are compatible.
234-
pub(crate) fn from_state_space_unsafe(state_space: CommitStateSpace) -> Self {
235-
Self { state_space }
236-
}
237-
238234
/// Add a replacement to `self`.
239235
///
240236
/// The effect of this is equivalent to applying `replacement` to the
@@ -314,6 +310,15 @@ impl PersistentHugr {
314310
}
315311
Ok(commit_id.expect("new_commits cannot be empty"))
316312
}
313+
}
314+
315+
impl<R> PersistentHugr<R> {
316+
/// Construct a [`PersistentHugr`] from a [`CommitStateSpace`].
317+
///
318+
/// Does not check that the commits are compatible.
319+
pub(crate) fn from_state_space_unsafe(state_space: CommitStateSpace<R>) -> Self {
320+
Self { state_space }
321+
}
317322

318323
/// Convert this `PersistentHugr` to a materialized Hugr by applying all
319324
/// commits in `self`.
@@ -373,12 +378,12 @@ impl PersistentHugr {
373378
}
374379

375380
/// Get a reference to the underlying state space of `self`.
376-
pub fn as_state_space(&self) -> &CommitStateSpace {
381+
pub fn as_state_space(&self) -> &CommitStateSpace<R> {
377382
&self.state_space
378383
}
379384

380385
/// Convert `self` into its underlying [`CommitStateSpace`].
381-
pub fn into_state_space(self) -> CommitStateSpace {
386+
pub fn into_state_space(self) -> CommitStateSpace<R> {
382387
self.state_space
383388
}
384389

@@ -654,7 +659,7 @@ impl PersistentHugr {
654659
}
655660
}
656661

657-
impl IntoIterator for PersistentHugr {
662+
impl<R> IntoIterator for PersistentHugr<R> {
658663
type Item = Commit;
659664

660665
type IntoIter = vec::IntoIter<Commit>;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//! Serialized format for [`PersistentHugr`]
2+
3+
use hugr_core::Hugr;
4+
5+
use crate::{CommitStateSpace, Resolver, state_space::serial::SerialCommitStateSpace};
6+
7+
use super::PersistentHugr;
8+
9+
/// Serialized format for [`PersistentHugr`]
10+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11+
pub struct SerialPersistentHugr<H, R> {
12+
/// The state space of all commits.
13+
state_space: SerialCommitStateSpace<H, R>,
14+
}
15+
16+
impl<R: Resolver> PersistentHugr<R> {
17+
/// Create a new [`CommitStateSpace`] from its serialized format
18+
pub fn from_serial<H: Into<Hugr>>(value: SerialPersistentHugr<H, R>) -> Self {
19+
let SerialPersistentHugr { state_space } = value;
20+
let state_space = CommitStateSpace::from_serial(state_space);
21+
Self { state_space }
22+
}
23+
24+
/// Convert a [`CommitStateSpace`] into its serialized format
25+
pub fn into_serial<H: From<Hugr>>(self) -> SerialPersistentHugr<H, R> {
26+
let Self { state_space } = self;
27+
let state_space = state_space.into_serial();
28+
SerialPersistentHugr { state_space }
29+
}
30+
31+
/// Create a serialized format from a reference to [`CommitStateSpace`]
32+
pub fn to_serial<H: From<Hugr>>(&self) -> SerialPersistentHugr<H, R> {
33+
let Self { state_space } = self;
34+
let state_space = state_space.to_serial();
35+
SerialPersistentHugr { state_space }
36+
}
37+
}
38+
39+
impl<H: From<Hugr>, R: Resolver> From<PersistentHugr<R>> for SerialPersistentHugr<H, R> {
40+
fn from(value: PersistentHugr<R>) -> Self {
41+
value.into_serial()
42+
}
43+
}
44+
45+
impl<H: Into<Hugr>, R: Resolver> From<SerialPersistentHugr<H, R>> for PersistentHugr<R> {
46+
fn from(value: SerialPersistentHugr<H, R>) -> Self {
47+
PersistentHugr::from_serial(value)
48+
}
49+
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use super::*;
54+
use crate::{
55+
CommitId, SerdeHashResolver,
56+
tests::{WrappedHugr, test_state_space},
57+
};
58+
59+
use rstest::rstest;
60+
61+
#[rstest]
62+
fn test_serde_persistent_hugr(
63+
test_state_space: (
64+
CommitStateSpace<SerdeHashResolver<WrappedHugr>>,
65+
[CommitId; 4],
66+
),
67+
) {
68+
let (state_space, [cm1, cm2, _, cm4]) = test_state_space;
69+
70+
let per_hugr = state_space.try_extract_hugr([cm1, cm2, cm4]).unwrap();
71+
let ser_per_hugr = per_hugr.to_serial::<WrappedHugr>();
72+
73+
insta::assert_snapshot!(serde_json::to_string_pretty(&ser_per_hugr).unwrap());
74+
}
75+
}

0 commit comments

Comments
 (0)