use std::collections::{HashMap, HashSet};
use panproto_gat::Name;
use panproto_inst::{Fan, Node, WInstance, wtype_restrict};
use panproto_schema::Edge;
use serde::{Deserialize, Serialize};
use crate::Lens;
use crate::error::LensError;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Complement {
pub dropped_nodes: HashMap<u32, Node>,
pub dropped_arcs: Vec<(u32, u32, Edge)>,
pub dropped_fans: Vec<Fan>,
pub contraction_choices: HashMap<(u32, u32), Edge>,
pub original_parent: HashMap<u32, u32>,
#[serde(default)]
pub source_fingerprint: u64,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub original_extra_fields: HashMap<u32, HashMap<String, panproto_inst::value::Value>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub arc_edges: HashMap<(u32, u32), Edge>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub original_values: HashMap<u32, Option<panproto_inst::value::FieldPresence>>,
#[serde(default, skip_serializing_if = "std::collections::HashSet::is_empty")]
pub synthesized_nodes: std::collections::HashSet<u32>,
}
impl Complement {
#[must_use]
pub fn empty() -> Self {
Self {
dropped_nodes: HashMap::new(),
dropped_arcs: Vec::new(),
dropped_fans: Vec::new(),
contraction_choices: HashMap::new(),
original_parent: HashMap::new(),
source_fingerprint: 0,
original_extra_fields: HashMap::new(),
arc_edges: HashMap::new(),
original_values: HashMap::new(),
synthesized_nodes: std::collections::HashSet::new(),
}
}
pub fn compose(&self, other: &Self) -> Result<Self, LensError> {
let source_fingerprint =
compose_fingerprint(self.source_fingerprint, other.source_fingerprint)?;
let mut dropped_nodes = self.dropped_nodes.clone();
merge_keyed_with_eq(
&mut dropped_nodes,
&other.dropped_nodes,
"dropped_nodes",
node_equiv,
)?;
let mut dropped_arcs = self.dropped_arcs.clone();
merge_vec_dedup(&mut dropped_arcs, &other.dropped_arcs);
let mut dropped_fans = self.dropped_fans.clone();
merge_vec_dedup(&mut dropped_fans, &other.dropped_fans);
let mut contraction_choices = self.contraction_choices.clone();
merge_keyed_with_eq(
&mut contraction_choices,
&other.contraction_choices,
"contraction_choices",
PartialEq::eq,
)?;
let mut original_parent = self.original_parent.clone();
merge_keyed_with_eq(
&mut original_parent,
&other.original_parent,
"original_parent",
PartialEq::eq,
)?;
let mut original_extra_fields = self.original_extra_fields.clone();
merge_keyed_with_eq(
&mut original_extra_fields,
&other.original_extra_fields,
"original_extra_fields",
extra_fields_equiv,
)?;
let mut arc_edges = self.arc_edges.clone();
merge_keyed_with_eq(&mut arc_edges, &other.arc_edges, "arc_edges", PartialEq::eq)?;
let mut original_values = self.original_values.clone();
merge_keyed_with_eq(
&mut original_values,
&other.original_values,
"original_values",
|a, b| presence_equiv(a.as_ref(), b.as_ref()),
)?;
let mut synthesized_nodes = self.synthesized_nodes.clone();
synthesized_nodes.extend(other.synthesized_nodes.iter().copied());
Ok(Self {
dropped_nodes,
dropped_arcs,
dropped_fans,
contraction_choices,
original_parent,
source_fingerprint,
original_extra_fields,
arc_edges,
original_values,
synthesized_nodes,
})
}
#[must_use]
pub fn is_compatible(&self, other: &Self) -> bool {
self.clone().compose(other).is_ok()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.dropped_nodes.is_empty()
&& self.dropped_arcs.is_empty()
&& self.dropped_fans.is_empty()
&& self.contraction_choices.is_empty()
&& self.original_parent.is_empty()
&& self.original_extra_fields.is_empty()
&& self.arc_edges.is_empty()
&& self.original_values.is_empty()
&& self.synthesized_nodes.is_empty()
}
}
fn compose_fingerprint(left: u64, right: u64) -> Result<u64, LensError> {
match (left, right) {
(0, _) | (_, 0) => Ok(left.max(right)),
(l, r) if l == r => Ok(l),
(l, r) => Err(LensError::ComplementFingerprintMismatch { left: l, right: r }),
}
}
fn merge_keyed_with_eq<K, V, F>(
target: &mut HashMap<K, V>,
source: &HashMap<K, V>,
kind: &'static str,
eq: F,
) -> Result<(), LensError>
where
K: Eq + std::hash::Hash + Clone + std::fmt::Debug,
V: Clone,
F: Fn(&V, &V) -> bool,
{
for (k, v) in source {
match target.get(k) {
None => {
target.insert(k.clone(), v.clone());
}
Some(existing) if eq(existing, v) => {}
Some(_) => {
return Err(LensError::ComplementConflict {
kind,
key: format!("{k:?}"),
});
}
}
}
Ok(())
}
fn merge_vec_dedup<T: Clone + PartialEq>(target: &mut Vec<T>, source: &[T]) {
for item in source {
if !target.contains(item) {
target.push(item.clone());
}
}
}
fn node_equiv(a: &Node, b: &Node) -> bool {
a.id == b.id
&& a.anchor == b.anchor
&& a.discriminator == b.discriminator
&& a.position == b.position
&& a.shape == b.shape
&& presence_equiv(a.value.as_ref(), b.value.as_ref())
&& extra_fields_equiv(&a.extra_fields, &b.extra_fields)
&& extra_fields_equiv(&a.annotations, &b.annotations)
}
pub(crate) fn extra_fields_equiv(
a: &HashMap<String, panproto_inst::value::Value>,
b: &HashMap<String, panproto_inst::value::Value>,
) -> bool {
if a.len() != b.len() {
return false;
}
a.iter()
.all(|(k, v)| b.get(k).is_some_and(|w| value_equiv(v, w)))
}
pub(crate) fn presence_equiv(
a: Option<&panproto_inst::value::FieldPresence>,
b: Option<&panproto_inst::value::FieldPresence>,
) -> bool {
use panproto_inst::value::FieldPresence;
match (a, b) {
(None, None)
| (Some(FieldPresence::Absent), Some(FieldPresence::Absent))
| (Some(FieldPresence::Null), Some(FieldPresence::Null)) => true,
(Some(FieldPresence::Present(x)), Some(FieldPresence::Present(y))) => value_equiv(x, y),
_ => false,
}
}
fn float_equiv(x: f64, y: f64) -> bool {
if x.is_nan() && y.is_nan() {
return true;
}
x.partial_cmp(&y) == Some(std::cmp::Ordering::Equal)
}
pub(crate) fn value_equiv(
a: &panproto_inst::value::Value,
b: &panproto_inst::value::Value,
) -> bool {
use panproto_inst::value::Value;
match (a, b) {
(Value::Float(x), Value::Float(y)) => float_equiv(*x, *y),
(Value::List(xs), Value::List(ys)) => {
xs.len() == ys.len() && xs.iter().zip(ys).all(|(x, y)| value_equiv(x, y))
}
(Value::Unknown(m1), Value::Unknown(m2)) => extra_fields_equiv(m1, m2),
(
Value::Opaque {
type_: t1,
fields: f1,
},
Value::Opaque {
type_: t2,
fields: f2,
},
) => t1 == t2 && extra_fields_equiv(f1, f2),
_ => a == b,
}
}
fn schema_fingerprint(schema: &panproto_schema::Schema) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
let mut vert_names: Vec<&str> = schema.vertices.keys().map(|n| &**n).collect();
vert_names.sort_unstable();
for v in &vert_names {
v.hash(&mut hasher);
}
schema.edges.len().hash(&mut hasher);
hasher.finish()
}
pub fn get(lens: &Lens, instance: &WInstance) -> Result<(WInstance, Complement), LensError> {
let view = wtype_restrict(instance, &lens.src_schema, &lens.tgt_schema, &lens.compiled)?;
let mut dropped_nodes = HashMap::new();
for (&id, node) in &instance.nodes {
if !view.nodes.contains_key(&id) {
dropped_nodes.insert(id, node.clone());
}
}
let mut dropped_arcs = Vec::new();
for arc in &instance.arcs {
let (parent, child, _) = arc;
if !view.nodes.contains_key(parent) || !view.nodes.contains_key(child) {
dropped_arcs.push(arc.clone());
}
}
let mut contraction_choices = HashMap::new();
for (parent, child, edge) in &view.arcs {
let was_direct = instance
.arcs
.iter()
.any(|(p, c, _)| p == parent && c == child);
if !was_direct {
contraction_choices.insert((*parent, *child), edge.clone());
}
}
let mut original_parent = HashMap::new();
for &id in view.nodes.keys() {
if let Some(&parent) = instance.parent_map.get(&id) {
original_parent.insert(id, parent);
}
}
let mut dropped_fans = Vec::new();
for fan in &instance.fans {
let parent_survives = view.nodes.contains_key(&fan.parent);
let all_children_survive = fan
.children
.values()
.all(|node_id| view.nodes.contains_key(node_id));
if !parent_survives || !all_children_survive {
dropped_fans.push(fan.clone());
}
}
let mut original_extra_fields = HashMap::new();
for &id in view.nodes.keys() {
if let Some(source_node) = instance.nodes.get(&id) {
if lens
.compiled
.field_transforms
.contains_key(&source_node.anchor)
{
original_extra_fields.insert(id, source_node.extra_fields.clone());
}
}
}
let mut original_values = HashMap::new();
for &id in view.nodes.keys() {
if let Some(source_node) = instance.nodes.get(&id) {
if lens
.compiled
.field_transforms
.get(&source_node.anchor)
.is_some_and(|ts| {
ts.iter().any(|t| {
matches!(t, panproto_inst::FieldTransform::ApplyExpr { key, .. } if key == "__value__")
})
})
{
original_values.insert(id, source_node.value.clone());
}
}
}
let mut arc_edges = HashMap::new();
for (parent, child, edge) in &instance.arcs {
if view.nodes.contains_key(parent) && view.nodes.contains_key(child) {
arc_edges.insert((*parent, *child), edge.clone());
}
}
let source_fingerprint = schema_fingerprint(&lens.src_schema);
let synthesized_nodes: std::collections::HashSet<u32> = view
.nodes
.keys()
.copied()
.filter(|id| !instance.nodes.contains_key(id))
.collect();
let complement = Complement {
dropped_nodes,
dropped_arcs,
dropped_fans,
contraction_choices,
original_parent,
source_fingerprint,
original_extra_fields,
arc_edges,
original_values,
synthesized_nodes,
};
Ok((view, complement))
}
pub fn put(lens: &Lens, view: &WInstance, complement: &Complement) -> Result<WInstance, LensError> {
if complement.source_fingerprint != 0 {
let expected = schema_fingerprint(&lens.src_schema);
if complement.source_fingerprint != expected {
return Err(LensError::ComplementMismatch {
detail: format!(
"source fingerprint mismatch: complement has {}, lens expects {}",
complement.source_fingerprint, expected
),
});
}
}
let mut nodes = HashMap::new();
let reverse_remap = build_reverse_remap(&lens.compiled.vertex_remap);
for (&id, node) in &view.nodes {
if complement.synthesized_nodes.contains(&id) {
continue;
}
let mut restored_node = node.clone();
if let Some(original_anchor) = reverse_remap.get(&node.anchor) {
restored_node.anchor.clone_from(original_anchor);
}
let src_anchor = reverse_remap.get(&node.anchor).unwrap_or(&node.anchor);
let transforms = lens.compiled.field_transforms.get(src_anchor);
if let Some(original_fields) = complement.original_extra_fields.get(&id) {
let view_fields = restored_node.extra_fields.clone();
restored_node.extra_fields.clone_from(original_fields);
if let Some(transforms) = transforms {
propagate_view_edits_through_inverse(&mut restored_node, &view_fields, transforms);
}
} else if let Some(transforms) = transforms {
apply_inverse_transforms(&mut restored_node, transforms);
}
if let Some(original_val) = complement.original_values.get(&id) {
restored_node.value.clone_from(original_val);
}
nodes.insert(id, restored_node);
}
for (&id, node) in &complement.dropped_nodes {
nodes.insert(id, node.clone());
}
let mut arcs = Vec::new();
let mut seen_pairs: HashSet<(u32, u32)> = HashSet::new();
for (&child_id, &original_parent) in &complement.original_parent {
if !nodes.contains_key(&child_id) || child_id == view.root {
continue;
}
if let Some(arc) = find_original_arc(
&lens.src_schema,
&nodes,
original_parent,
child_id,
&complement.contraction_choices,
&complement.arc_edges,
) && seen_pairs.insert((arc.0, arc.1))
{
arcs.push(arc);
}
}
for arc in &complement.dropped_arcs {
let (parent, child, _) = arc;
if nodes.contains_key(parent)
&& nodes.contains_key(child)
&& seen_pairs.insert((*parent, *child))
{
arcs.push(arc.clone());
}
}
let mut fans: Vec<Fan> = view
.fans
.iter()
.map(|fan| {
let mut restored_fan = fan.clone();
if let Some(original_he) = reverse_remap.get(fan.hyper_edge_id.as_str()) {
restored_fan.hyper_edge_id = original_he.to_string();
}
restored_fan
})
.collect();
for fan in &complement.dropped_fans {
let parent_present = nodes.contains_key(&fan.parent);
let all_children_present = fan
.children
.values()
.all(|node_id| nodes.contains_key(node_id));
if parent_present && all_children_present {
fans.push(fan.clone());
}
}
let schema_root = reverse_remap
.get(&view.schema_root)
.cloned()
.unwrap_or_else(|| view.schema_root.clone());
Ok(WInstance::new(nodes, arcs, fans, view.root, schema_root))
}
fn propagate_view_edits_through_inverse(
node: &mut Node,
view_fields: &std::collections::HashMap<String, panproto_inst::value::Value>,
transforms: &[panproto_inst::FieldTransform],
) {
use panproto_inst::FieldTransform;
let mut working: std::collections::HashMap<String, panproto_inst::value::Value> =
view_fields.clone();
for transform in transforms.iter().rev() {
match transform {
FieldTransform::RenameField { old_key, new_key } => {
if let Some(val) = working.remove(new_key) {
node.extra_fields.insert(old_key.clone(), val.clone());
working.insert(old_key.clone(), val);
}
}
FieldTransform::ApplyExpr {
key,
inverse: Some(inv_expr),
..
} => {
if key == "__value__" {
continue;
}
if working.contains_key(key) {
let env = panproto_inst::build_env_from_extra_fields(&working);
let config = panproto_expr::EvalConfig::default();
if let Ok(result) = panproto_expr::eval(inv_expr, &env, &config) {
let inverted = panproto_inst::expr_literal_to_value(&result);
node.extra_fields.insert(key.clone(), inverted.clone());
working.insert(key.clone(), inverted);
}
}
}
FieldTransform::ComputeField {
target_key,
inverse: Some(inv_expr),
..
} if working.contains_key(target_key) => {
let env = panproto_inst::build_env_from_extra_fields(&working);
let config = panproto_expr::EvalConfig::default();
if let Ok(result) = panproto_expr::eval(inv_expr, &env, &config) {
let inverted = panproto_inst::expr_literal_to_value(&result);
node.extra_fields.insert(target_key.clone(), inverted);
}
}
FieldTransform::AddField { key, .. } => {
node.extra_fields.remove(key);
}
FieldTransform::PathTransform { path, inner } if path.is_empty() => {
propagate_view_edits_through_inverse(
node,
view_fields,
std::slice::from_ref(inner),
);
}
_ => {}
}
}
}
fn apply_inverse_transforms(node: &mut Node, transforms: &[panproto_inst::FieldTransform]) {
use panproto_inst::FieldTransform;
for transform in transforms.iter().rev() {
match transform {
FieldTransform::ApplyExpr {
key,
inverse: Some(inv_expr),
..
} => {
if key == "__value__" {
if let Some(panproto_inst::value::FieldPresence::Present(val)) = &node.value {
let input = panproto_inst::value_to_expr_literal(val);
let env = panproto_expr::Env::new()
.extend(std::sync::Arc::from("v"), input.clone())
.extend(std::sync::Arc::from("__value__"), input);
let config = panproto_expr::EvalConfig::default();
if let Ok(result) = panproto_expr::eval(inv_expr, &env, &config) {
node.value = Some(panproto_inst::value::FieldPresence::Present(
panproto_inst::expr_literal_to_value(&result),
));
}
}
} else if node.extra_fields.contains_key(key) {
let env = panproto_inst::build_env_from_extra_fields(&node.extra_fields);
let config = panproto_expr::EvalConfig::default();
if let Ok(result) = panproto_expr::eval(inv_expr, &env, &config) {
node.extra_fields
.insert(key.clone(), panproto_inst::expr_literal_to_value(&result));
}
}
}
FieldTransform::ComputeField {
target_key,
inverse: Some(inv_expr),
..
} if node.extra_fields.contains_key(target_key) => {
let env = panproto_inst::build_env_from_extra_fields(&node.extra_fields);
let config = panproto_expr::EvalConfig::default();
if let Ok(result) = panproto_expr::eval(inv_expr, &env, &config) {
node.extra_fields.insert(
target_key.clone(),
panproto_inst::expr_literal_to_value(&result),
);
}
}
FieldTransform::RenameField { old_key, new_key } => {
if let Some(val) = node.extra_fields.remove(new_key) {
node.extra_fields.insert(old_key.clone(), val);
}
}
FieldTransform::AddField { key, .. } => {
node.extra_fields.remove(key);
}
FieldTransform::PathTransform { path, inner } if path.is_empty() => {
apply_inverse_transforms(node, std::slice::from_ref(inner));
}
_ => {}
}
}
}
fn build_reverse_remap(forward: &HashMap<Name, Name>) -> HashMap<Name, Name> {
forward
.iter()
.map(|(k, v)| (v.clone(), k.clone()))
.collect()
}
fn find_original_arc(
src_schema: &panproto_schema::Schema,
nodes: &HashMap<u32, Node>,
parent_id: u32,
child_id: u32,
contraction_choices: &HashMap<(u32, u32), Edge>,
arc_edges: &HashMap<(u32, u32), Edge>,
) -> Option<(u32, u32, Edge)> {
if let Some(edge) = arc_edges.get(&(parent_id, child_id)) {
return Some((parent_id, child_id, edge.clone()));
}
if let Some(edge) = contraction_choices.get(&(parent_id, child_id)) {
return Some((parent_id, child_id, edge.clone()));
}
let parent_node = nodes.get(&parent_id)?;
let child_node = nodes.get(&child_id)?;
let edges = src_schema.edges_between(&parent_node.anchor, &child_node.anchor);
if edges.len() == 1 {
Some((parent_id, child_id, edges[0].clone()))
} else {
edges.first().map(|e| (parent_id, child_id, e.clone()))
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tests::{identity_lens, three_node_instance, three_node_schema};
#[test]
fn get_with_identity_lens_produces_empty_complement() {
let schema = three_node_schema();
let lens = identity_lens(&schema);
let instance = three_node_instance();
let (view, complement) =
get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
assert_eq!(view.node_count(), instance.node_count());
assert!(
complement.dropped_nodes.is_empty(),
"no nodes should be dropped by identity lens"
);
}
#[test]
fn get_then_put_round_trips_identity() {
let schema = three_node_schema();
let lens = identity_lens(&schema);
let instance = three_node_instance();
let (view, complement) =
get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
let restored = put(&lens, &view, &complement).unwrap_or_else(|e| panic!("put failed: {e}"));
assert_eq!(
restored.node_count(),
instance.node_count(),
"restored should have same node count"
);
assert_eq!(restored.root, instance.root, "restored root should match");
}
#[test]
fn complement_is_empty_for_identity() {
let complement = Complement::empty();
assert!(complement.is_empty());
}
mod partial_monoid {
use super::*;
use panproto_inst::Node as InstNode;
use panproto_schema::Edge;
fn mk_node(id: u32, anchor: &str) -> InstNode {
InstNode::new(id, anchor)
}
fn mk_edge(src: &str, tgt: &str, kind: &str) -> Edge {
Edge {
src: src.into(),
tgt: tgt.into(),
kind: kind.into(),
name: None,
}
}
fn singleton_dropped_node(id: u32, anchor: &str) -> Complement {
let mut c = Complement::empty();
c.dropped_nodes.insert(id, mk_node(id, anchor));
c
}
fn singleton_arc_edge(parent: u32, child: u32, kind: &str) -> Complement {
let mut c = Complement::empty();
c.arc_edges.insert((parent, child), mk_edge("a", "b", kind));
c
}
#[test]
fn empty_is_left_identity() {
let c = singleton_dropped_node(1, "x");
let composed = Complement::empty().compose(&c).expect("compatible");
assert_eq!(composed.dropped_nodes.len(), 1);
assert!(composed.dropped_nodes.contains_key(&1));
}
#[test]
fn empty_is_right_identity() {
let c = singleton_dropped_node(1, "x");
let composed = c.compose(&Complement::empty()).expect("compatible");
assert_eq!(composed.dropped_nodes.len(), 1);
}
#[test]
fn idempotent_on_equal_entries() {
let c = singleton_dropped_node(7, "v7");
let composed = c.compose(&c).expect("equal entries idempotent");
assert_eq!(composed.dropped_nodes.len(), 1);
}
#[test]
fn rejects_distinct_entries_on_same_key() {
let a = singleton_dropped_node(3, "left");
let b = singleton_dropped_node(3, "right");
assert!(matches!(
a.compose(&b),
Err(LensError::ComplementConflict {
kind: "dropped_nodes",
..
})
));
assert!(!a.is_compatible(&b));
}
#[test]
fn rejects_cross_fingerprint_when_both_set() {
let mut a = Complement::empty();
a.source_fingerprint = 0xAAAA;
let mut b = Complement::empty();
b.source_fingerprint = 0xBBBB;
assert!(matches!(
a.compose(&b),
Err(LensError::ComplementFingerprintMismatch { .. })
));
}
#[test]
fn propagates_fingerprint_when_only_one_set() {
let mut a = Complement::empty();
a.source_fingerprint = 0xC0DE;
let b = Complement::empty();
let r = a.compose(&b).expect("compatible");
assert_eq!(r.source_fingerprint, 0xC0DE);
let r2 = b.compose(&a).expect("compatible");
assert_eq!(r2.source_fingerprint, 0xC0DE);
}
#[test]
fn associative_on_disjoint_keys() {
let a = singleton_dropped_node(1, "v1");
let b = singleton_dropped_node(2, "v2");
let c = singleton_arc_edge(3, 4, "prop");
let left = a
.compose(&b)
.expect("ab compatible")
.compose(&c)
.expect("(ab)c compatible");
let right = a
.compose(&b.compose(&c).expect("bc compatible"))
.expect("a(bc) compatible");
assert_eq!(left.dropped_nodes.len(), right.dropped_nodes.len());
assert_eq!(left.arc_edges.len(), right.arc_edges.len());
for (k, v) in &left.dropped_nodes {
let other = right.dropped_nodes.get(k).expect("present on right");
assert!(node_equiv(v, other));
}
for (k, v) in &left.arc_edges {
assert_eq!(right.arc_edges.get(k), Some(v));
}
}
#[test]
fn commutative_on_disjoint_keys() {
let a = singleton_dropped_node(10, "x");
let b = singleton_dropped_node(20, "y");
let ab = a.compose(&b).expect("compatible");
let ba = b.compose(&a).expect("compatible");
assert_eq!(ab.dropped_nodes.len(), ba.dropped_nodes.len());
for (k, v) in &ab.dropped_nodes {
let other = ba.dropped_nodes.get(k).expect("present");
assert!(node_equiv(v, other));
}
}
}
}