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