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::{HugrError, 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    /// The entrypoint of the HUGR.
100    ///
101    /// For backwards compatibility, if `None` the entrypoint is set to the root
102    /// of the node hierarchy.
103    #[serde(default)]
104    entrypoint: Option<Node>,
105}
106
107/// Errors that can occur while serializing a HUGR.
108#[derive(Debug, Clone, PartialEq, Error)]
109#[non_exhaustive]
110pub enum HUGRSerializationError {
111    /// Unexpected hierarchy error.
112    #[error("Failed to attach child to parent: {0}.")]
113    AttachError(#[from] AttachError),
114    /// Failed to add edge.
115    #[error("Failed to build edge when deserializing: {0}.")]
116    LinkError(#[from] LinkError),
117    /// Edges without port offsets cannot be present in operations without non-dataflow ports.
118    #[error(
119        "Cannot connect an {dir:?} edge without port offset to node {node} with operation type {op_type}."
120    )]
121    MissingPortOffset {
122        /// The node that has the port without offset.
123        node: Node,
124        /// The direction of the port without an offset
125        dir: Direction,
126        /// The operation type of the node.
127        op_type: OpType,
128    },
129    /// Edges with wrong node indices
130    #[error("The edge endpoint {node} is not a node in the graph.")]
131    UnknownEdgeNode {
132        /// The node that has the port without offset.
133        node: Node,
134    },
135    /// First node in node list must be the HUGR root.
136    #[error("The first node in the node list has parent {0}, should be itself (index 0)")]
137    FirstNodeNotRoot(Node),
138    /// Failed to deserialize the HUGR.
139    #[error(transparent)]
140    HugrError(#[from] HugrError),
141}
142
143impl Hugr {
144    /// Serializes the HUGR using a serde encoder.
145    ///
146    /// This is an internal API, used to generate the JSON variant of the HUGR envelope format.
147    pub(crate) fn serde_serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
148    where
149        S: serde::Serializer,
150    {
151        let shg: SerHugrLatest = self.try_into().map_err(serde::ser::Error::custom)?;
152        let versioned = Versioned::new_latest(shg);
153        versioned.serialize(serializer)
154    }
155
156    /// Deserializes the HUGR using a serde decoder.
157    ///
158    /// This is an internal API, used to read the JSON variant of the HUGR envelope format.
159    pub(crate) fn serde_deserialize<'de, D>(deserializer: D) -> Result<Hugr, D::Error>
160    where
161        D: Deserializer<'de>,
162    {
163        let versioned = Versioned::deserialize(deserializer)?;
164        let shl: SerHugrLatest = versioned.upgrade().map_err(serde::de::Error::custom)?;
165        shl.try_into().map_err(serde::de::Error::custom)
166    }
167}
168
169/// Deerialize the HUGR using a serde decoder.
170///
171/// This API is unstable API and will be removed in the future.
172#[deprecated(
173    since = "0.20.0",
174    note = "This API is unstable and will be removed in the future.
175            Use `Hugr::load` or the `AsStringEnvelope` adaptor instead."
176)]
177#[doc(hidden)]
178pub fn serde_deserialize_hugr<'de, D>(deserializer: D) -> Result<Hugr, D::Error>
179where
180    D: Deserializer<'de>,
181{
182    Hugr::serde_deserialize(deserializer)
183}
184
185impl TryFrom<&Hugr> for SerHugrLatest {
186    type Error = HUGRSerializationError;
187
188    fn try_from(hugr: &Hugr) -> Result<Self, Self::Error> {
189        // We compact the operation nodes during the serialization process,
190        // and ignore the copy nodes.
191        let mut node_rekey: HashMap<Node, Node> = HashMap::with_capacity(hugr.num_nodes());
192        for (order, node) in hugr.canonical_order(hugr.module_root()).enumerate() {
193            node_rekey.insert(node, portgraph::NodeIndex::new(order).into());
194        }
195
196        let mut nodes = vec![None; hugr.num_nodes()];
197        let mut metadata = vec![None; hugr.num_nodes()];
198        for n in hugr.nodes() {
199            let parent = node_rekey[&hugr.get_parent(n).unwrap_or(n)];
200            let opt = hugr.get_optype(n);
201            let new_node = node_rekey[&n].index();
202            nodes[new_node] = Some(NodeSer {
203                parent,
204                op: opt.clone(),
205            });
206            metadata[new_node].clone_from(hugr.metadata.get(n.into_portgraph()));
207        }
208        let nodes = nodes
209            .into_iter()
210            .collect::<Option<Vec<_>>>()
211            .expect("Could not reach one of the nodes");
212
213        let find_offset = |node: Node, offset: usize, dir: Direction, hugr: &Hugr| {
214            let op = hugr.get_optype(node);
215            let is_value_port = offset < op.value_port_count(dir);
216            let is_static_input = op.static_port(dir).is_some_and(|p| p.index() == offset);
217            let offset = (is_value_port || is_static_input).then_some(offset as u16);
218            (node_rekey[&node], offset)
219        };
220
221        let edges: Vec<_> = hugr
222            .nodes()
223            .flat_map(|node| {
224                hugr.node_ports(node, Direction::Outgoing)
225                    .enumerate()
226                    .flat_map(move |(src_offset, port)| {
227                        let src = find_offset(node, src_offset, Direction::Outgoing, hugr);
228                        hugr.linked_ports(node, port).map(move |(tgt_node, tgt)| {
229                            let tgt = find_offset(tgt_node, tgt.index(), Direction::Incoming, hugr);
230                            [src, tgt]
231                        })
232                    })
233            })
234            .collect();
235
236        let encoder = Some(format!("hugr-rs v{}", env!("CARGO_PKG_VERSION")));
237
238        Ok(Self {
239            nodes,
240            edges,
241            metadata: Some(metadata),
242            encoder,
243            entrypoint: Some(node_rekey[&hugr.entrypoint()]),
244        })
245    }
246}
247
248impl TryFrom<SerHugrLatest> for Hugr {
249    type Error = HUGRSerializationError;
250    fn try_from(
251        SerHugrLatest {
252            nodes,
253            edges,
254            metadata,
255            encoder: _,
256            entrypoint,
257        }: SerHugrLatest,
258    ) -> Result<Self, Self::Error> {
259        // Root must be first node
260        let mut nodes = nodes.into_iter();
261        let NodeSer {
262            parent: root_parent,
263            op: root_type,
264            ..
265        } = nodes.next().unwrap();
266        if root_parent.index() != 0 {
267            return Err(HUGRSerializationError::FirstNodeNotRoot(root_parent));
268        }
269        // if there are any unconnected ports or copy nodes the capacity will be
270        // an underestimate
271        let mut hugr = Hugr::with_capacity(root_type, nodes.len(), edges.len() * 2)?;
272
273        // Since the new Hugr may add some nodes to contain the root (if the
274        // encoded file did not have a module at the root), we need a function
275        // to map the node indices.
276        let padding_nodes = hugr.entrypoint.index();
277        let hugr_node =
278            |node: Node| -> Node { portgraph::NodeIndex::new(node.index() + padding_nodes).into() };
279
280        for node_ser in nodes {
281            hugr.add_node_with_parent(hugr_node(node_ser.parent), node_ser.op);
282        }
283
284        if let Some(entrypoint) = entrypoint {
285            hugr.set_entrypoint(entrypoint);
286        }
287
288        if let Some(metadata) = metadata {
289            for (node_idx, metadata) in metadata.into_iter().enumerate() {
290                if let Some(metadata) = metadata {
291                    let node = hugr_node(portgraph::NodeIndex::new(node_idx).into());
292                    hugr.metadata[node.into_portgraph()] = Some(metadata);
293                }
294            }
295        }
296
297        let unwrap_offset = |node: Node, offset, dir, hugr: &Hugr| -> Result<usize, Self::Error> {
298            if !hugr.graph.contains_node(node.into_portgraph()) {
299                return Err(HUGRSerializationError::UnknownEdgeNode { node });
300            }
301            let offset = if let Some(offset) = offset {
302                offset as usize
303            } else {
304                let op_type = hugr.get_optype(node);
305                op_type
306                    .other_port(dir)
307                    .ok_or(HUGRSerializationError::MissingPortOffset {
308                        node,
309                        dir,
310                        op_type: op_type.clone(),
311                    })?
312                    .index()
313            };
314            Ok(offset)
315        };
316        for [(src, from_offset), (dst, to_offset)] in edges {
317            let src = hugr_node(src);
318            let dst = hugr_node(dst);
319
320            let src_port = unwrap_offset(src, from_offset, Direction::Outgoing, &hugr)?;
321            let dst_port = unwrap_offset(dst, to_offset, Direction::Incoming, &hugr)?;
322
323            hugr.connect(src, src_port, dst, dst_port);
324        }
325
326        Ok(hugr)
327    }
328}
329
330#[cfg(all(test, not(miri)))]
331// Miri doesn't run the extension registration required by `typetag` for
332// registering `CustomConst`s.  https://github.com/rust-lang/miri/issues/450
333pub mod test;