hugr_core/hugr/
serialize.rs

1//! Serialization definition for [`Hugr`]
2//! [`Hugr`]: crate::hugr::Hugr
3
4use serde::de::DeserializeOwned;
5use std::collections::HashMap;
6use thiserror::Error;
7
8use crate::core::NodeIndex;
9use crate::hugr::Hugr;
10use crate::ops::OpType;
11use crate::{Node, PortIndex};
12use portgraph::hierarchy::AttachError;
13use portgraph::{Direction, LinkError, PortView};
14
15use serde::{Deserialize, Deserializer, Serialize};
16
17use self::upgrade::UpgradeError;
18
19use super::{HugrMut, HugrView, NodeMetadataMap};
20
21mod upgrade;
22
23/// A wrapper over the available HUGR serialization formats.
24///
25/// The implementation of `Serialize` for `Hugr` encodes the graph in the most
26/// recent version of the format. We keep the `Deserialize` implementations for
27/// older versions to allow for backwards compatibility.
28///
29/// The Generic `SerHugr` is always instantiated to the most recent version of
30/// the format outside this module.
31///
32/// Make sure to order the variants from newest to oldest, as the deserializer
33/// will try to deserialize them in order.
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(tag = "version", rename_all = "lowercase")]
36enum Versioned<SerHugr = SerHugrLatest> {
37    #[serde(skip_serializing)]
38    /// Version 0 of the HUGR serialization format.
39    V0,
40
41    V1(serde_json::Value),
42    V2(serde_json::Value),
43    Live(SerHugr),
44
45    #[serde(skip_serializing)]
46    #[serde(other)]
47    Unsupported,
48}
49
50impl<T> Versioned<T> {
51    pub fn new_latest(t: T) -> Self {
52        Self::Live(t)
53    }
54}
55
56impl<T: DeserializeOwned> Versioned<T> {
57    fn upgrade(self) -> Result<T, UpgradeError> {
58        // go is polymorphic in D. When we are upgrading to the latest version
59        // D is T. When we are upgrading to a version which is not the latest D
60        // is serde_json::Value.
61        #[allow(unused)]
62        fn go<D: serde::de::DeserializeOwned>(v: serde_json::Value) -> Result<D, UpgradeError> {
63            serde_json::from_value(v).map_err(Into::into)
64        }
65        loop {
66            match self {
67                Self::V0 => Err(UpgradeError::KnownVersionUnsupported("0".into()))?,
68                // the upgrade lines remain unchanged when adding a new constructor
69                // Self::V1(json) => self = Self::V2(upgrade::v1_to_v2(json).and_then(go)?),
70                Self::V1(_) => Err(UpgradeError::KnownVersionUnsupported("1".into()))?,
71                Self::V2(_) => Err(UpgradeError::KnownVersionUnsupported("2".into()))?,
72                Self::Live(ser_hugr) => return Ok(ser_hugr),
73                Versioned::Unsupported => Err(UpgradeError::UnknownVersionUnsupported)?,
74            }
75        }
76    }
77}
78
79#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
80struct NodeSer {
81    parent: Node,
82    #[serde(flatten)]
83    op: OpType,
84}
85
86/// Version 1 of the HUGR serialization format.
87#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
88struct SerHugrLatest {
89    /// For each node: (parent, node_operation)
90    nodes: Vec<NodeSer>,
91    /// for each edge: (src, src_offset, tgt, tgt_offset)
92    edges: Vec<[(Node, Option<u16>); 2]>,
93    /// for each node: (metadata)
94    #[serde(default)]
95    metadata: Option<Vec<Option<NodeMetadataMap>>>,
96    /// A metadata field with the package identifier that encoded the HUGR.
97    #[serde(default)]
98    encoder: Option<String>,
99}
100
101/// Errors that can occur while serializing a HUGR.
102#[derive(Debug, Clone, PartialEq, Error)]
103#[non_exhaustive]
104pub enum HUGRSerializationError {
105    /// Unexpected hierarchy error.
106    #[error("Failed to attach child to parent: {0}.")]
107    AttachError(#[from] AttachError),
108    /// Failed to add edge.
109    #[error("Failed to build edge when deserializing: {0}.")]
110    LinkError(#[from] LinkError),
111    /// Edges without port offsets cannot be present in operations without non-dataflow ports.
112    #[error("Cannot connect an {dir:?} edge without port offset to node {node} with operation type {op_type}.")]
113    MissingPortOffset {
114        /// The node that has the port without offset.
115        node: Node,
116        /// The direction of the port without an offset
117        dir: Direction,
118        /// The operation type of the node.
119        op_type: OpType,
120    },
121    /// Edges with wrong node indices
122    #[error("The edge endpoint {node} is not a node in the graph.")]
123    UnknownEdgeNode {
124        /// The node that has the port without offset.
125        node: Node,
126    },
127    /// First node in node list must be the HUGR root.
128    #[error("The first node in the node list has parent {0}, should be itself (index 0)")]
129    FirstNodeNotRoot(Node),
130}
131
132impl Serialize for Hugr {
133    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
134    where
135        S: serde::Serializer,
136    {
137        let shg: SerHugrLatest = self.try_into().map_err(serde::ser::Error::custom)?;
138        let versioned = Versioned::new_latest(shg);
139        versioned.serialize(serializer)
140    }
141}
142
143impl<'de> Deserialize<'de> for Hugr {
144    fn deserialize<D>(deserializer: D) -> Result<Hugr, D::Error>
145    where
146        D: Deserializer<'de>,
147    {
148        let versioned = Versioned::deserialize(deserializer)?;
149        let shl: SerHugrLatest = versioned.upgrade().map_err(serde::de::Error::custom)?;
150        shl.try_into().map_err(serde::de::Error::custom)
151    }
152}
153
154impl TryFrom<&Hugr> for SerHugrLatest {
155    type Error = HUGRSerializationError;
156
157    fn try_from(hugr: &Hugr) -> Result<Self, Self::Error> {
158        // We compact the operation nodes during the serialization process,
159        // and ignore the copy nodes.
160        let mut node_rekey: HashMap<Node, Node> = HashMap::with_capacity(hugr.node_count());
161        for (order, node) in hugr.canonical_order(hugr.root()).enumerate() {
162            node_rekey.insert(node, portgraph::NodeIndex::new(order).into());
163        }
164
165        let mut nodes = vec![None; hugr.node_count()];
166        let mut metadata = vec![None; hugr.node_count()];
167        for n in hugr.nodes() {
168            let parent = node_rekey[&hugr.get_parent(n).unwrap_or(n)];
169            let opt = hugr.get_optype(n);
170            let new_node = node_rekey[&n].index();
171            nodes[new_node] = Some(NodeSer {
172                parent,
173                op: opt.clone(),
174            });
175            metadata[new_node].clone_from(hugr.metadata.get(n.pg_index()));
176        }
177        let nodes = nodes
178            .into_iter()
179            .collect::<Option<Vec<_>>>()
180            .expect("Could not reach one of the nodes");
181
182        let find_offset = |node: Node, offset: usize, dir: Direction, hugr: &Hugr| {
183            let op = hugr.get_optype(node);
184            let is_value_port = offset < op.value_port_count(dir);
185            let is_static_input = op.static_port(dir).is_some_and(|p| p.index() == offset);
186            let offset = (is_value_port || is_static_input).then_some(offset as u16);
187            (node_rekey[&node], offset)
188        };
189
190        let edges: Vec<_> = hugr
191            .nodes()
192            .flat_map(|node| {
193                hugr.node_ports(node, Direction::Outgoing)
194                    .enumerate()
195                    .flat_map(move |(src_offset, port)| {
196                        let src = find_offset(node, src_offset, Direction::Outgoing, hugr);
197                        hugr.linked_ports(node, port).map(move |(tgt_node, tgt)| {
198                            let tgt = find_offset(tgt_node, tgt.index(), Direction::Incoming, hugr);
199                            [src, tgt]
200                        })
201                    })
202            })
203            .collect();
204
205        let encoder = Some(format!("hugr-rs v{}", env!("CARGO_PKG_VERSION")));
206
207        Ok(Self {
208            nodes,
209            edges,
210            metadata: Some(metadata),
211            encoder,
212        })
213    }
214}
215
216impl TryFrom<SerHugrLatest> for Hugr {
217    type Error = HUGRSerializationError;
218    fn try_from(
219        SerHugrLatest {
220            nodes,
221            edges,
222            metadata,
223            encoder: _,
224        }: SerHugrLatest,
225    ) -> Result<Self, Self::Error> {
226        // Root must be first node
227        let mut nodes = nodes.into_iter();
228        let NodeSer {
229            parent: root_parent,
230            op: root_type,
231            ..
232        } = nodes.next().unwrap();
233        if root_parent.index() != 0 {
234            return Err(HUGRSerializationError::FirstNodeNotRoot(root_parent));
235        }
236        // if there are any unconnected ports or copy nodes the capacity will be
237        // an underestimate
238        let mut hugr = Hugr::with_capacity(root_type, nodes.len(), edges.len() * 2);
239
240        for node_ser in nodes {
241            hugr.add_node_with_parent(node_ser.parent, node_ser.op);
242        }
243
244        if let Some(metadata) = metadata {
245            for (node, metadata) in metadata.into_iter().enumerate() {
246                if let Some(metadata) = metadata {
247                    let node = portgraph::NodeIndex::new(node);
248                    hugr.metadata[node] = Some(metadata);
249                }
250            }
251        }
252
253        let unwrap_offset = |node: Node, offset, dir, hugr: &Hugr| -> Result<usize, Self::Error> {
254            if !hugr.graph.contains_node(node.pg_index()) {
255                return Err(HUGRSerializationError::UnknownEdgeNode { node });
256            }
257            let offset = match offset {
258                Some(offset) => offset as usize,
259                None => {
260                    let op_type = hugr.get_optype(node);
261                    op_type
262                        .other_port(dir)
263                        .ok_or(HUGRSerializationError::MissingPortOffset {
264                            node,
265                            dir,
266                            op_type: op_type.clone(),
267                        })?
268                        .index()
269                }
270            };
271            Ok(offset)
272        };
273        for [(src, from_offset), (dst, to_offset)] in edges {
274            let src_port = unwrap_offset(src, from_offset, Direction::Outgoing, &hugr)?;
275            let dst_port = unwrap_offset(dst, to_offset, Direction::Incoming, &hugr)?;
276
277            hugr.connect(src, src_port, dst, dst_port);
278        }
279
280        Ok(hugr)
281    }
282}
283
284#[cfg(all(test, not(miri)))]
285// Miri doesn't run the extension registration required by `typetag` for
286// registering `CustomConst`s.  https://github.com/rust-lang/miri/issues/450
287pub mod test;