use crate::node::graph::Graph;
use crate::node::{self, Node};
use crate::visit;
use gantz_ca::{
ContentAddr, DataGraph, Datum, DatumError, GraphAddr, NodeData, Registry, SectionId, datum,
};
use petgraph::visit::EdgeRef;
use serde::{Serialize, de::DeserializeOwned};
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug)]
pub struct ReifiedGraphs<N> {
graphs: HashMap<GraphAddr, Graph<N>>,
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum EraseNodeError {
#[error("node serde error: {0}")]
Datum(#[from] DatumError),
#[error("node serde produced a value without a `type`-tagged map")]
Untagged,
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("node {node_ix}: {source}")]
pub struct EraseError {
pub node_ix: usize,
#[source]
pub source: EraseNodeError,
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("node type `{tag}`: {source}")]
pub struct ReifyNodeError {
pub tag: String,
#[source]
pub source: DatumError,
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("node {node_ix}: {source}")]
pub struct ReifyError {
pub node_ix: usize,
#[source]
pub source: ReifyNodeError,
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("graph {graph}: {source}")]
pub struct EnsureError {
pub graph: GraphAddr,
#[source]
pub source: ReifyError,
}
impl<N> ReifiedGraphs<N> {
pub fn new() -> Self {
Self {
graphs: HashMap::new(),
}
}
pub fn get(&self, addr: &GraphAddr) -> Option<&Graph<N>> {
self.graphs.get(addr)
}
pub fn contains(&self, addr: &GraphAddr) -> bool {
self.graphs.contains_key(addr)
}
pub fn retain_live(&mut self, live: &gantz_ca::LiveSet) {
self.graphs.retain(|addr, _| live.graphs.contains(addr));
}
}
impl<N> ReifiedGraphs<N> {
pub fn ensure_with(
&mut self,
reg: &Registry,
seeds: impl IntoIterator<Item = ContentAddr>,
reify_node: impl Fn(&NodeData) -> Result<N, ReifyNodeError>,
) -> Result<(), EnsureError> {
let mut queue: VecDeque<GraphAddr> = seeds.into_iter().map(GraphAddr::from).collect();
while let Some(addr) = queue.pop_front() {
if self.graphs.contains_key(&addr) {
continue;
}
let Some(dg) = reg.graph(&addr) else { continue };
queue.extend(
dg.node_weights()
.flat_map(|n| n.refs.iter().copied().map(GraphAddr::from)),
);
let g = reify_with(dg, &reify_node).map_err(|source| EnsureError {
graph: addr,
source,
})?;
self.graphs.insert(addr, g);
}
Ok(())
}
pub fn ensure_all_with(
&mut self,
reg: &Registry,
reify_node: impl Fn(&NodeData) -> Result<N, ReifyNodeError>,
) -> Vec<EnsureError> {
let mut errs = vec![];
for (addr, dg) in reg.graphs() {
if self.graphs.contains_key(addr) {
continue;
}
match reify_with(dg, &reify_node) {
Ok(g) => {
self.graphs.insert(*addr, g);
}
Err(source) => errs.push(EnsureError {
graph: *addr,
source,
}),
}
}
errs
}
}
impl<N: DeserializeOwned> ReifiedGraphs<N> {
pub fn ensure(
&mut self,
reg: &Registry,
seeds: impl IntoIterator<Item = ContentAddr>,
) -> Result<(), EnsureError> {
self.ensure_with(reg, seeds, reify_node)
}
pub fn ensure_all(&mut self, reg: &Registry) -> Vec<EnsureError> {
self.ensure_all_with(reg, reify_node)
}
}
impl<N> Default for ReifiedGraphs<N> {
fn default() -> Self {
Self::new()
}
}
pub fn erase_node<N>(node: &N) -> Result<NodeData, EraseNodeError>
where
N: Serialize + Node,
{
let datum = datum::to_datum(node)?;
let Datum::Map(mut entries) = datum else {
return Err(EraseNodeError::Untagged);
};
let Some(ix) = entries.iter().position(|(k, _)| k == "type") else {
return Err(EraseNodeError::Untagged);
};
let (_, tag) = entries.remove(ix);
let Datum::Str(tag) = tag else {
return Err(EraseNodeError::Untagged);
};
Ok(node_data(tag, entries, node))
}
pub fn erase_node_tagged<N>(tag: &str, node: &N) -> Result<NodeData, EraseNodeError>
where
N: Serialize + Node,
{
let mut entries = match datum::to_datum(node)? {
Datum::Map(entries) => entries,
Datum::Null => vec![],
_ => return Err(EraseNodeError::Untagged),
};
entries.retain(|(k, _)| k != "type");
Ok(node_data(tag.to_string(), entries, node))
}
pub fn erase_node_typed<T>(node: &T) -> Result<NodeData, EraseNodeError>
where
T: gantz_nodetag::NodeTag + Serialize + Node,
{
erase_node_tagged(T::TAG, node)
}
fn node_data<N: Node>(tag: String, fields: Vec<(String, Datum)>, node: &N) -> NodeData {
let (refs, blobs) = node_out_refs(node);
let mut node_data = NodeData {
tag,
data: Datum::Map(fields),
refs,
blobs,
};
node_data.canonicalize();
node_data
}
pub fn reify_node<N>(node_data: &NodeData) -> Result<N, ReifyNodeError>
where
N: DeserializeOwned,
{
let err = |source| ReifyNodeError {
tag: node_data.tag.clone(),
source,
};
let Datum::Map(fields) = node_data.data.clone() else {
return Err(err(serde::de::Error::custom("node data is not a map")));
};
let datum = Datum::tagged(&node_data.tag, fields);
datum::from_datum(datum).map_err(err)
}
pub fn reify_node_concrete<T>(node_data: &NodeData) -> Result<T, ReifyNodeError>
where
T: DeserializeOwned,
{
let err = |source| ReifyNodeError {
tag: node_data.tag.clone(),
source,
};
let Datum::Map(_) = node_data.data else {
return Err(err(serde::de::Error::custom("node data is not a map")));
};
datum::from_datum(node_data.data.clone()).map_err(err)
}
pub fn erase_with_addr<N>(g: &Graph<N>) -> Result<(DataGraph, GraphAddr), EraseError>
where
N: Serialize + Node,
{
let dg = erase(g)?;
let addr = gantz_ca::graph_addr(&dg);
Ok((dg, addr))
}
pub fn erase<N>(g: &Graph<N>) -> Result<DataGraph, EraseError>
where
N: Serialize + Node,
{
let mut out = DataGraph::with_capacity(g.node_count(), g.edge_count());
for (node_ix, w) in g.node_weights().enumerate() {
let node_data = erase_node(w).map_err(|source| EraseError { node_ix, source })?;
out.add_node(node_data);
}
for e in g.edge_references() {
out.add_edge(e.source(), e.target(), *e.weight());
}
Ok(out)
}
pub fn reify<N>(g: &DataGraph) -> Result<Graph<N>, ReifyError>
where
N: DeserializeOwned,
{
reify_with(g, reify_node)
}
pub fn reify_with<N>(
g: &DataGraph,
reify_node: impl Fn(&NodeData) -> Result<N, ReifyNodeError>,
) -> Result<Graph<N>, ReifyError> {
let mut out = Graph::with_capacity(g.node_count(), g.edge_count());
for (node_ix, node_data) in g.node_weights().enumerate() {
let node = reify_node(node_data).map_err(|source| ReifyError { node_ix, source })?;
out.add_node(node);
}
for e in g.edge_references() {
out.add_edge(e.source(), e.target(), *e.weight());
}
Ok(out)
}
fn node_out_refs<N: Node>(node: &N) -> (Vec<ContentAddr>, Vec<(SectionId, ContentAddr)>) {
fn no_node(_: &ContentAddr) -> Option<&'static dyn Node> {
None
}
let mut addrs = HashSet::new();
let mut blobs = HashSet::new();
node::visit(
visit::Ctx::new(&no_node, &[], &[]),
node,
&mut visit::RequiredAddrs { addrs: &mut addrs },
);
node::visit(
visit::Ctx::new(&no_node, &[], &[]),
node,
&mut visit::RequiredBlobs { blobs: &mut blobs },
);
let mut refs: Vec<_> = addrs.into_iter().collect();
refs.sort();
let mut blobs: Vec<_> = blobs.into_iter().collect();
blobs.sort();
(refs, blobs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::ExprResult;
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
enum TestNode {
Num { v: i64 },
Link { addr: ContentAddr },
}
impl Node for TestNode {
fn expr(&self, _: node::ExprCtx) -> ExprResult {
unimplemented!("not compiled in these tests")
}
fn required_addrs(&self) -> Vec<ContentAddr> {
match self {
TestNode::Num { .. } => vec![],
TestNode::Link { addr } => vec![*addr],
}
}
}
fn num(v: i64) -> TestNode {
TestNode::Num { v }
}
fn graph(nodes: impl IntoIterator<Item = TestNode>) -> Graph<TestNode> {
let mut g = Graph::default();
let ixs: Vec<_> = nodes.into_iter().map(|n| g.add_node(n)).collect();
for w in ixs.windows(2) {
g.add_edge(w[0], w[1], gantz_ca::Edge::from((0, 0)));
}
g
}
#[test]
fn erase_node_splits_tag_and_extracts_refs() {
let nd = erase_node(&num(42)).unwrap();
assert_eq!(nd.tag, "Num");
assert_eq!(nd.data, Datum::Map(vec![("v".into(), Datum::I64(42))]));
assert!(nd.refs.is_empty() && nd.blobs.is_empty());
assert!(nd.is_canonical());
let target = ContentAddr([7; 32]);
let nd = erase_node(&TestNode::Link { addr: target }).unwrap();
assert_eq!(nd.tag, "Link");
assert_eq!(nd.refs, vec![target]);
}
#[test]
fn erase_node_tagged_matches_erase_node() {
let link = TestNode::Link {
addr: ContentAddr([7; 32]),
};
for (tag, node) in [("Num", num(42)), ("Link", link)] {
let tagged = erase_node_tagged(tag, &node).unwrap();
let split = erase_node(&node).unwrap();
assert_eq!(tagged, split, "typed and box erasure diverge for {tag}");
assert_eq!(tagged.content_addr(), split.content_addr());
}
}
#[test]
fn concrete_erase_reify_round_trips() {
use gantz_nodetag::NodeTag;
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize, NodeTag)]
struct Plain {
v: i64,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize, NodeTag)]
struct Unit;
impl Node for Plain {
fn expr(&self, _: node::ExprCtx) -> ExprResult {
unimplemented!("not compiled in these tests")
}
}
impl Node for Unit {
fn expr(&self, _: node::ExprCtx) -> ExprResult {
unimplemented!("not compiled in these tests")
}
}
let nd = erase_node_typed(&Plain { v: 7 }).unwrap();
assert_eq!(nd.tag, "Plain");
assert_eq!(nd.data, Datum::Map(vec![("v".into(), Datum::I64(7))]));
assert!(nd.is_canonical());
assert_eq!(reify_node_concrete::<Plain>(&nd).unwrap(), Plain { v: 7 });
let nd = erase_node_typed(&Unit).unwrap();
assert_eq!(nd.tag, "Unit");
assert_eq!(nd.data, Datum::Map(vec![]));
assert_eq!(reify_node_concrete::<Unit>(&nd).unwrap(), Unit);
}
#[test]
fn graph_round_trips_preserving_structure() {
let mut g = graph([num(1), num(2), num(3)]);
g.add_edge(0.into(), 2.into(), gantz_ca::Edge::from((1, 1)));
let dg = erase(&g).unwrap();
let back: Graph<TestNode> = reify(&dg).unwrap();
let weights: Vec<_> = back.node_weights().cloned().collect();
assert_eq!(weights, vec![num(1), num(2), num(3)]);
let edges: Vec<_> = back
.edge_references()
.map(|e| (e.source().index(), e.target().index(), *e.weight()))
.collect();
let expected: Vec<_> = g
.edge_references()
.map(|e| (e.source().index(), e.target().index(), *e.weight()))
.collect();
assert_eq!(edges, expected);
}
#[test]
fn reify_unknown_tag_names_node_and_tag() {
let mut dg = erase(&graph([num(1)])).unwrap();
dg.node_weights_mut().for_each(|n| n.tag = "Mystery".into());
let err = reify::<TestNode>(&dg).unwrap_err();
assert_eq!(err.node_ix, 0);
assert_eq!(err.source.tag, "Mystery");
assert!(err.to_string().contains("Mystery"), "{err}");
}
#[test]
fn ensure_reifies_transitive_refs_and_ignores_unresolved() {
let mut reg = Registry::default();
let leaf = reg.add_graph(erase(&graph([num(1)])).unwrap());
let mid = {
let g = graph([TestNode::Link { addr: leaf.into() }, num(2)]);
reg.add_graph(erase(&g).unwrap())
};
let root = {
let mut g = graph([TestNode::Link { addr: mid.into() }]);
g.add_node(TestNode::Link {
addr: ContentAddr([9; 32]),
});
reg.add_graph(erase(&g).unwrap())
};
let mut cache = ReifiedGraphs::<TestNode>::new();
cache.ensure(®, [root.into()]).unwrap();
assert!(cache.contains(&root) && cache.contains(&mid) && cache.contains(&leaf));
assert!(!cache.contains(&GraphAddr::from(ContentAddr([9; 32]))));
assert_eq!(cache.get(&leaf).unwrap().node_count(), 1);
cache.ensure(®, [root.into()]).unwrap();
}
}