Skip to main content

gantz_core/
data.rs

1//! The codec between typed nodes and the registry's erased
2//! [`NodeData`]/[`DataGraph`] representation, plus the reified-graph cache.
3//!
4//! The registry stores graphs as plain data. Typed nodes cross that boundary
5//! here: [`erase`] erases a working graph for storage and [`reify`]
6//! reifies one for editing and compilation. Erasure rides the node set's
7//! tag-dispatched serde (`gantz_format::impl_node_set_serde!`) through
8//! [`Datum`], so the node-set manifest is the codec: a node type is storable
9//! exactly when it is listed there.
10
11use crate::node::graph::Graph;
12use crate::node::{self, Node};
13use crate::visit;
14use gantz_ca::{
15    ContentAddr, DataGraph, Datum, DatumError, GraphAddr, NodeData, Registry, SectionId, datum,
16};
17use petgraph::visit::EdgeRef;
18use serde::{Serialize, de::DeserializeOwned};
19use std::collections::{HashMap, HashSet, VecDeque};
20
21/// An append-only cache of reified registry graphs, keyed by graph address.
22///
23/// Content addressing makes entries immutable: an address names exactly one
24/// graph forever, so the cache never invalidates. [`ReifiedGraphs::retain_live`]
25/// may drop entries to bound memory after a prune.
26///
27/// Intended use is two-phase: [`ReifiedGraphs::ensure`] everything a pass can
28/// reach (requires `&mut self`), then serve the whole pass immutably through
29/// [`ReifiedGraphs::get`] borrows (e.g. behind a `GetNode` closure).
30#[derive(Debug)]
31pub struct ReifiedGraphs<N> {
32    graphs: HashMap<GraphAddr, Graph<N>>,
33}
34
35/// Failure to erase a node: its serde did not produce a `type`-tagged map
36/// (i.e. it is not the node set's tag-dispatched serde), or errored outright.
37#[derive(Clone, Debug, thiserror::Error)]
38pub enum EraseNodeError {
39    /// The node's own serde failed.
40    #[error("node serde error: {0}")]
41    Datum(#[from] DatumError),
42    /// The node's serde produced a value without a `type`-tagged map.
43    #[error("node serde produced a value without a `type`-tagged map")]
44    Untagged,
45}
46
47/// Failure to erase one of a graph's nodes.
48#[derive(Clone, Debug, thiserror::Error)]
49#[error("node {node_ix}: {source}")]
50pub struct EraseError {
51    /// The graph index of the node that failed to erase.
52    pub node_ix: usize,
53    /// The node-level failure.
54    #[source]
55    pub source: EraseNodeError,
56}
57
58/// Failure to reify a typed node from its data form: the tag is unknown to
59/// the node set, or the fields fail the node's own deserialization.
60#[derive(Clone, Debug, thiserror::Error)]
61#[error("node type `{tag}`: {source}")]
62pub struct ReifyNodeError {
63    /// The wire tag of the node that failed to decode.
64    pub tag: String,
65    /// The decode failure.
66    #[source]
67    pub source: DatumError,
68}
69
70/// Failure to reify one of a graph's nodes.
71#[derive(Clone, Debug, thiserror::Error)]
72#[error("node {node_ix}: {source}")]
73pub struct ReifyError {
74    /// The graph index of the node that failed to decode.
75    pub node_ix: usize,
76    /// The node-level failure.
77    #[source]
78    pub source: ReifyNodeError,
79}
80
81/// Failure to reify a registry graph while filling the cache.
82#[derive(Clone, Debug, thiserror::Error)]
83#[error("graph {graph}: {source}")]
84pub struct EnsureError {
85    /// The address of the registry graph that failed to reify.
86    pub graph: GraphAddr,
87    /// The graph-level failure.
88    #[source]
89    pub source: ReifyError,
90}
91
92impl<N> ReifiedGraphs<N> {
93    /// An empty cache.
94    pub fn new() -> Self {
95        Self {
96            graphs: HashMap::new(),
97        }
98    }
99
100    /// The reified graph at the given address, if it has been ensured.
101    pub fn get(&self, addr: &GraphAddr) -> Option<&Graph<N>> {
102        self.graphs.get(addr)
103    }
104
105    /// Whether the given address has been reified.
106    pub fn contains(&self, addr: &GraphAddr) -> bool {
107        self.graphs.contains_key(addr)
108    }
109
110    /// Drop entries outside the given live set to bound memory after a prune.
111    pub fn retain_live(&mut self, live: &gantz_ca::LiveSet) {
112        self.graphs.retain(|addr, _| live.graphs.contains(addr));
113    }
114}
115
116impl<N> ReifiedGraphs<N> {
117    /// Reify the given seed addresses and every graph they transitively
118    /// reference, decoding each node weight through `reify_node`.
119    ///
120    /// References are resolved through the stored graphs' [`NodeData::refs`]
121    /// columns, a pure data walk: nothing is decoded to *find* the set.
122    /// Addresses that don't resolve to registry graphs (e.g. builtin node
123    /// addresses in a node's refs) are ignored, as are already-cached graphs.
124    pub fn ensure_with(
125        &mut self,
126        reg: &Registry,
127        seeds: impl IntoIterator<Item = ContentAddr>,
128        reify_node: impl Fn(&NodeData) -> Result<N, ReifyNodeError>,
129    ) -> Result<(), EnsureError> {
130        let mut queue: VecDeque<GraphAddr> = seeds.into_iter().map(GraphAddr::from).collect();
131        while let Some(addr) = queue.pop_front() {
132            if self.graphs.contains_key(&addr) {
133                continue;
134            }
135            let Some(dg) = reg.graph(&addr) else { continue };
136            queue.extend(
137                dg.node_weights()
138                    .flat_map(|n| n.refs.iter().copied().map(GraphAddr::from)),
139            );
140            let g = reify_with(dg, &reify_node).map_err(|source| EnsureError {
141                graph: addr,
142                source,
143            })?;
144            self.graphs.insert(addr, g);
145        }
146        Ok(())
147    }
148
149    /// Reify every graph in the registry's column, best effort, decoding each
150    /// node weight through `reify_node`.
151    ///
152    /// Graphs that fail to reify (e.g. an unknown tag from a domain not
153    /// compiled in) are skipped and reported, and remain cache misses that
154    /// lookups degrade over the same way as any missing node.
155    pub fn ensure_all_with(
156        &mut self,
157        reg: &Registry,
158        reify_node: impl Fn(&NodeData) -> Result<N, ReifyNodeError>,
159    ) -> Vec<EnsureError> {
160        let mut errs = vec![];
161        for (addr, dg) in reg.graphs() {
162            if self.graphs.contains_key(addr) {
163                continue;
164            }
165            match reify_with(dg, &reify_node) {
166                Ok(g) => {
167                    self.graphs.insert(*addr, g);
168                }
169                Err(source) => errs.push(EnsureError {
170                    graph: *addr,
171                    source,
172                }),
173            }
174        }
175        errs
176    }
177}
178
179impl<N: DeserializeOwned> ReifiedGraphs<N> {
180    /// [`ensure_with`][Self::ensure_with] over the node set's own serde
181    /// ([`reify_node`]).
182    pub fn ensure(
183        &mut self,
184        reg: &Registry,
185        seeds: impl IntoIterator<Item = ContentAddr>,
186    ) -> Result<(), EnsureError> {
187        self.ensure_with(reg, seeds, reify_node)
188    }
189
190    /// [`ensure_all_with`][Self::ensure_all_with] over the node set's own
191    /// serde ([`reify_node`]).
192    pub fn ensure_all(&mut self, reg: &Registry) -> Vec<EnsureError> {
193        self.ensure_all_with(reg, reify_node)
194    }
195}
196
197impl<N> Default for ReifiedGraphs<N> {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203/// Erase a typed node to data.
204///
205/// Runs the node's own (tag-dispatched) serde to a [`Datum`], splits the
206/// `"type"` tag out, and extracts the node's direct outgoing references from
207/// its own reporting ([`Node::required_addrs`]/[`Node::required_blobs`],
208/// including physically nested nodes). The result is canonical, so its
209/// [`NodeData::content_addr`] is the node's one network-wide address.
210pub fn erase_node<N>(node: &N) -> Result<NodeData, EraseNodeError>
211where
212    N: Serialize + Node,
213{
214    let datum = datum::to_datum(node)?;
215    let Datum::Map(mut entries) = datum else {
216        return Err(EraseNodeError::Untagged);
217    };
218    let Some(ix) = entries.iter().position(|(k, _)| k == "type") else {
219        return Err(EraseNodeError::Untagged);
220    };
221    let (_, tag) = entries.remove(ix);
222    let Datum::Str(tag) = tag else {
223        return Err(EraseNodeError::Untagged);
224    };
225    Ok(node_data(tag, entries, node))
226}
227
228/// Erase a typed node to data under an externally supplied wire tag.
229///
230/// The typed-path counterpart of [`erase_node`]: where that rides the node
231/// set's tag-dispatched box serde and splits the `"type"` entry out, this
232/// runs the node's own concrete serde and takes the tag from the caller
233/// (usually its [`NodeTag`](gantz_nodetag::NodeTag), via
234/// [`erase_node_typed`]). The node's serde must produce a map - a
235/// unit-struct node's `Null` counts as the empty map, matching the box
236/// path's flattened form - else the erasure fails as
237/// [`EraseNodeError::Untagged`]. A serde that embeds its own `"type"` entry
238/// (e.g. an internally tagged enum) has it stripped, keeping [`NodeData::tag`]
239/// the single source of the tag. Both paths yield the same canonical
240/// [`NodeData`], and thus the same content address.
241pub fn erase_node_tagged<N>(tag: &str, node: &N) -> Result<NodeData, EraseNodeError>
242where
243    N: Serialize + Node,
244{
245    let mut entries = match datum::to_datum(node)? {
246        Datum::Map(entries) => entries,
247        Datum::Null => vec![],
248        _ => return Err(EraseNodeError::Untagged),
249    };
250    entries.retain(|(k, _)| k != "type");
251    Ok(node_data(tag.to_string(), entries, node))
252}
253
254/// Erase a typed node to data under its own declared
255/// [`NodeTag`](gantz_nodetag::NodeTag).
256///
257/// See [`erase_node_tagged`].
258pub fn erase_node_typed<T>(node: &T) -> Result<NodeData, EraseNodeError>
259where
260    T: gantz_nodetag::NodeTag + Serialize + Node,
261{
262    erase_node_tagged(T::TAG, node)
263}
264
265/// Assemble the canonical [`NodeData`] for `node` from its wire tag and
266/// tag-stripped field entries: the shared tail of [`erase_node`] and
267/// [`erase_node_tagged`].
268fn node_data<N: Node>(tag: String, fields: Vec<(String, Datum)>, node: &N) -> NodeData {
269    let (refs, blobs) = node_out_refs(node);
270    let mut node_data = NodeData {
271        tag,
272        data: Datum::Map(fields),
273        refs,
274        blobs,
275    };
276    node_data.canonicalize();
277    node_data
278}
279
280/// Reify one typed node: rebuild the tagged map and run node-set serde.
281pub fn reify_node<N>(node_data: &NodeData) -> Result<N, ReifyNodeError>
282where
283    N: DeserializeOwned,
284{
285    let err = |source| ReifyNodeError {
286        tag: node_data.tag.clone(),
287        source,
288    };
289    let Datum::Map(fields) = node_data.data.clone() else {
290        return Err(err(serde::de::Error::custom("node data is not a map")));
291    };
292    // The tag leads, which is the node-set deserializer's streaming fast path.
293    let datum = Datum::tagged(&node_data.tag, fields);
294    datum::from_datum(datum).map_err(err)
295}
296
297/// Reify one node at its concrete type: run the type's own serde over the
298/// stored fields.
299///
300/// The typed-path counterpart of [`reify_node`]: no `"type"` tag is
301/// prepended, since a concrete type's serde must never see one - tag
302/// dispatch belongs to the caller (e.g. matching [`NodeData::tag`] against
303/// each candidate type's [`NodeTag`](gantz_nodetag::NodeTag)).
304pub fn reify_node_concrete<T>(node_data: &NodeData) -> Result<T, ReifyNodeError>
305where
306    T: DeserializeOwned,
307{
308    let err = |source| ReifyNodeError {
309        tag: node_data.tag.clone(),
310        source,
311    };
312    let Datum::Map(_) = node_data.data else {
313        return Err(err(serde::de::Error::custom("node data is not a map")));
314    };
315    datum::from_datum(node_data.data.clone()).map_err(err)
316}
317
318/// Erase a typed graph and compute its registry address in one pass.
319///
320/// Registry graph addresses are ALWAYS computed on the erased form: typed
321/// nodes carry no content addressing of their own. Any site that compares
322/// or mints a registry address for a typed working graph goes through here
323/// (or erases first).
324pub fn erase_with_addr<N>(g: &Graph<N>) -> Result<(DataGraph, GraphAddr), EraseError>
325where
326    N: Serialize + Node,
327{
328    let dg = erase(g)?;
329    let addr = gantz_ca::graph_addr(&dg);
330    Ok((dg, addr))
331}
332
333/// Erase a typed graph for storage: node weights through [`erase_node`],
334/// indices and edges preserved verbatim.
335pub fn erase<N>(g: &Graph<N>) -> Result<DataGraph, EraseError>
336where
337    N: Serialize + Node,
338{
339    let mut out = DataGraph::with_capacity(g.node_count(), g.edge_count());
340    for (node_ix, w) in g.node_weights().enumerate() {
341        let node_data = erase_node(w).map_err(|source| EraseError { node_ix, source })?;
342        out.add_node(node_data);
343    }
344    for e in g.edge_references() {
345        out.add_edge(e.source(), e.target(), *e.weight());
346    }
347    Ok(out)
348}
349
350/// Reify a typed graph from its stored data form: node weights through
351/// [`reify_node`], indices and edges preserved verbatim.
352pub fn reify<N>(g: &DataGraph) -> Result<Graph<N>, ReifyError>
353where
354    N: DeserializeOwned,
355{
356    reify_with(g, reify_node)
357}
358
359/// Reify a typed graph from its stored data form, decoding each node weight
360/// through `reify_node`: the codec-parameterized twin of [`reify`].
361pub fn reify_with<N>(
362    g: &DataGraph,
363    reify_node: impl Fn(&NodeData) -> Result<N, ReifyNodeError>,
364) -> Result<Graph<N>, ReifyError> {
365    let mut out = Graph::with_capacity(g.node_count(), g.edge_count());
366    for (node_ix, node_data) in g.node_weights().enumerate() {
367        let node = reify_node(node_data).map_err(|source| ReifyError { node_ix, source })?;
368        out.add_node(node);
369    }
370    for e in g.edge_references() {
371        out.add_edge(e.source(), e.target(), *e.weight());
372    }
373    Ok(out)
374}
375
376/// A node's direct outgoing references: its own reporting plus that of its
377/// physically nested nodes.
378///
379/// The absent node lookup stops reference nodes from following their target
380/// into other graphs, keeping the result direct - the reachability walk owns
381/// the transitive closure.
382fn node_out_refs<N: Node>(node: &N) -> (Vec<ContentAddr>, Vec<(SectionId, ContentAddr)>) {
383    fn no_node(_: &ContentAddr) -> Option<&'static dyn Node> {
384        None
385    }
386    let mut addrs = HashSet::new();
387    let mut blobs = HashSet::new();
388    node::visit(
389        visit::Ctx::new(&no_node, &[], &[]),
390        node,
391        &mut visit::RequiredAddrs { addrs: &mut addrs },
392    );
393    node::visit(
394        visit::Ctx::new(&no_node, &[], &[]),
395        node,
396        &mut visit::RequiredBlobs { blobs: &mut blobs },
397    );
398    let mut refs: Vec<_> = addrs.into_iter().collect();
399    refs.sort();
400    let mut blobs: Vec<_> = blobs.into_iter().collect();
401    blobs.sort();
402    (refs, blobs)
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use crate::node::ExprResult;
409
410    /// A minimal tag-dispatched node set: an internally-tagged enum serializes
411    /// to exactly the `"type"`-tagged map shape the node-set macro produces.
412    #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
413    #[serde(tag = "type")]
414    enum TestNode {
415        Num { v: i64 },
416        Link { addr: ContentAddr },
417    }
418
419    impl Node for TestNode {
420        fn expr(&self, _: node::ExprCtx) -> ExprResult {
421            unimplemented!("not compiled in these tests")
422        }
423
424        fn required_addrs(&self) -> Vec<ContentAddr> {
425            match self {
426                TestNode::Num { .. } => vec![],
427                TestNode::Link { addr } => vec![*addr],
428            }
429        }
430    }
431
432    fn num(v: i64) -> TestNode {
433        TestNode::Num { v }
434    }
435
436    fn graph(nodes: impl IntoIterator<Item = TestNode>) -> Graph<TestNode> {
437        let mut g = Graph::default();
438        let ixs: Vec<_> = nodes.into_iter().map(|n| g.add_node(n)).collect();
439        for w in ixs.windows(2) {
440            g.add_edge(w[0], w[1], gantz_ca::Edge::from((0, 0)));
441        }
442        g
443    }
444
445    #[test]
446    fn erase_node_splits_tag_and_extracts_refs() {
447        let nd = erase_node(&num(42)).unwrap();
448        assert_eq!(nd.tag, "Num");
449        assert_eq!(nd.data, Datum::Map(vec![("v".into(), Datum::I64(42))]));
450        assert!(nd.refs.is_empty() && nd.blobs.is_empty());
451        assert!(nd.is_canonical());
452
453        let target = ContentAddr([7; 32]);
454        let nd = erase_node(&TestNode::Link { addr: target }).unwrap();
455        assert_eq!(nd.tag, "Link");
456        assert_eq!(nd.refs, vec![target]);
457    }
458
459    /// The typed erasure path must match the box path byte-for-byte: with the
460    /// internally tagged `TestNode` standing in for the node-set serde, the
461    /// tag-supplied erasure of each variant equals [`erase_node`]'s
462    /// tag-splitting erasure (same data, same refs, same content address).
463    #[test]
464    fn erase_node_tagged_matches_erase_node() {
465        let link = TestNode::Link {
466            addr: ContentAddr([7; 32]),
467        };
468        for (tag, node) in [("Num", num(42)), ("Link", link)] {
469            let tagged = erase_node_tagged(tag, &node).unwrap();
470            let split = erase_node(&node).unwrap();
471            assert_eq!(tagged, split, "typed and box erasure diverge for {tag}");
472            assert_eq!(tagged.content_addr(), split.content_addr());
473        }
474    }
475
476    /// Concrete typed nodes round-trip without ever seeing a `"type"` field:
477    /// a fields struct and a unit struct (whose typed serde yields `Null`,
478    /// erased as the empty map) both erase via their [`NodeTag`] and reify
479    /// back at their concrete type.
480    #[test]
481    fn concrete_erase_reify_round_trips() {
482        use gantz_nodetag::NodeTag;
483
484        #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize, NodeTag)]
485        struct Plain {
486            v: i64,
487        }
488
489        #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize, NodeTag)]
490        struct Unit;
491
492        impl Node for Plain {
493            fn expr(&self, _: node::ExprCtx) -> ExprResult {
494                unimplemented!("not compiled in these tests")
495            }
496        }
497
498        impl Node for Unit {
499            fn expr(&self, _: node::ExprCtx) -> ExprResult {
500                unimplemented!("not compiled in these tests")
501            }
502        }
503
504        let nd = erase_node_typed(&Plain { v: 7 }).unwrap();
505        assert_eq!(nd.tag, "Plain");
506        assert_eq!(nd.data, Datum::Map(vec![("v".into(), Datum::I64(7))]));
507        assert!(nd.is_canonical());
508        assert_eq!(reify_node_concrete::<Plain>(&nd).unwrap(), Plain { v: 7 });
509
510        let nd = erase_node_typed(&Unit).unwrap();
511        assert_eq!(nd.tag, "Unit");
512        assert_eq!(nd.data, Datum::Map(vec![]));
513        assert_eq!(reify_node_concrete::<Unit>(&nd).unwrap(), Unit);
514    }
515
516    #[test]
517    fn graph_round_trips_preserving_structure() {
518        let mut g = graph([num(1), num(2), num(3)]);
519        // A parallel edge and a distinct socket pairing survive.
520        g.add_edge(0.into(), 2.into(), gantz_ca::Edge::from((1, 1)));
521        let dg = erase(&g).unwrap();
522        let back: Graph<TestNode> = reify(&dg).unwrap();
523        let weights: Vec<_> = back.node_weights().cloned().collect();
524        assert_eq!(weights, vec![num(1), num(2), num(3)]);
525        let edges: Vec<_> = back
526            .edge_references()
527            .map(|e| (e.source().index(), e.target().index(), *e.weight()))
528            .collect();
529        let expected: Vec<_> = g
530            .edge_references()
531            .map(|e| (e.source().index(), e.target().index(), *e.weight()))
532            .collect();
533        assert_eq!(edges, expected);
534    }
535
536    #[test]
537    fn reify_unknown_tag_names_node_and_tag() {
538        let mut dg = erase(&graph([num(1)])).unwrap();
539        dg.node_weights_mut().for_each(|n| n.tag = "Mystery".into());
540        let err = reify::<TestNode>(&dg).unwrap_err();
541        assert_eq!(err.node_ix, 0);
542        assert_eq!(err.source.tag, "Mystery");
543        assert!(err.to_string().contains("Mystery"), "{err}");
544    }
545
546    #[test]
547    fn ensure_reifies_transitive_refs_and_ignores_unresolved() {
548        let mut reg = Registry::default();
549        let leaf = reg.add_graph(erase(&graph([num(1)])).unwrap());
550        let mid = {
551            let g = graph([TestNode::Link { addr: leaf.into() }, num(2)]);
552            reg.add_graph(erase(&g).unwrap())
553        };
554        let root = {
555            // One resolvable ref and one dangling (builtin-style) addr.
556            let mut g = graph([TestNode::Link { addr: mid.into() }]);
557            g.add_node(TestNode::Link {
558                addr: ContentAddr([9; 32]),
559            });
560            reg.add_graph(erase(&g).unwrap())
561        };
562
563        let mut cache = ReifiedGraphs::<TestNode>::new();
564        cache.ensure(&reg, [root.into()]).unwrap();
565        assert!(cache.contains(&root) && cache.contains(&mid) && cache.contains(&leaf));
566        assert!(!cache.contains(&GraphAddr::from(ContentAddr([9; 32]))));
567        assert_eq!(cache.get(&leaf).unwrap().node_count(), 1);
568
569        // Ensuring again is a no-op walk over cached entries.
570        cache.ensure(&reg, [root.into()]).unwrap();
571    }
572}