use std::collections::HashMap;
use panproto_gat::Name;
use panproto_schema::Schema;
use super::{Anchor, StrategyTag};
const WL_CONFIDENCE: f64 = 0.9;
#[must_use]
pub fn wl_anchors(src: &Schema, tgt: &Schema, iterations: usize) -> Vec<Anchor> {
let src_colors = refine_colors(src, iterations);
let tgt_colors = refine_colors(tgt, iterations);
let mut src_by_color: HashMap<[u8; 32], Vec<&Name>> = HashMap::new();
for (id, color) in &src_colors {
src_by_color.entry(*color).or_default().push(id);
}
let mut tgt_by_color: HashMap<[u8; 32], Vec<&Name>> = HashMap::new();
for (id, color) in &tgt_colors {
tgt_by_color.entry(*color).or_default().push(id);
}
let mut matched: Vec<([u8; 32], &Name, &Name)> = Vec::new();
for (color, src_ids) in &src_by_color {
if src_ids.len() != 1 {
continue;
}
let Some(tgt_ids) = tgt_by_color.get(color) else {
continue;
};
if tgt_ids.len() != 1 {
continue;
}
matched.push((*color, src_ids[0], tgt_ids[0]));
}
matched.sort_by(|a, b| a.1.as_str().cmp(b.1.as_str()));
matched
.into_iter()
.map(|(_color, s, t)| Anchor {
src: s.clone(),
tgt: t.clone(),
confidence: WL_CONFIDENCE,
strategy: StrategyTag::WlRefinement,
explanation: format!(
"WL refinement singleton color class: {} ↔ {}",
s.as_str(),
t.as_str()
),
})
.collect()
}
fn refine_colors(schema: &Schema, iterations: usize) -> HashMap<Name, [u8; 32]> {
let mut colors: HashMap<Name, [u8; 32]> = schema
.vertices
.iter()
.map(|(id, v)| (id.clone(), initial_color(schema, id, &v.kind)))
.collect();
for _ in 0..iterations {
let next: HashMap<Name, [u8; 32]> = colors
.keys()
.map(|id| (id.clone(), refine_step(schema, id, &colors)))
.collect();
if next == colors {
break;
}
colors = next;
}
colors
}
fn initial_color(schema: &Schema, id: &Name, kind: &Name) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(b"v0|");
hasher.update(kind.as_str().as_bytes());
hasher.update(b"|");
if let Some(cs) = schema.constraints.get(id) {
let mut pairs: Vec<(&str, &str)> = cs
.iter()
.map(|c| (c.sort.as_str(), c.value.as_str()))
.collect();
pairs.sort_unstable();
pairs.dedup();
for (s, v) in pairs {
let s_len = u64::try_from(s.len()).unwrap_or(u64::MAX);
let v_len = u64::try_from(v.len()).unwrap_or(u64::MAX);
hasher.update(&s_len.to_le_bytes());
hasher.update(s.as_bytes());
hasher.update(&v_len.to_le_bytes());
hasher.update(v.as_bytes());
}
}
*hasher.finalize().as_bytes()
}
const MISSING_SENTINEL: [u8; 32] = [0xFF; 32];
fn refine_step(schema: &Schema, id: &Name, colors: &HashMap<Name, [u8; 32]>) -> [u8; 32] {
let mut triples: Vec<(u8, [u8; 32], &str, &str)> = Vec::new();
for edge in schema.outgoing_edges(id) {
debug_assert!(
colors.contains_key(&edge.tgt),
"edge tgt missing from colors map: {} -> {}",
id.as_str(),
edge.tgt.as_str(),
);
let neighbor_color = colors.get(&edge.tgt).copied().unwrap_or(MISSING_SENTINEL);
let label = edge.name.as_deref().unwrap_or("");
triples.push((b'>', neighbor_color, label, edge.kind.as_str()));
}
for edge in schema.incoming_edges(id) {
debug_assert!(
colors.contains_key(&edge.src),
"edge src missing from colors map: {} <- {}",
id.as_str(),
edge.src.as_str(),
);
let neighbor_color = colors.get(&edge.src).copied().unwrap_or(MISSING_SENTINEL);
let label = edge.name.as_deref().unwrap_or("");
triples.push((b'<', neighbor_color, label, edge.kind.as_str()));
}
triples.sort_unstable();
let mut hasher = blake3::Hasher::new();
hasher.update(b"v1|");
let own = colors.get(id).copied().unwrap_or([0u8; 32]);
hasher.update(&own);
hasher.update(b"|");
for (dir, nc, label, kind) in triples {
hasher.update(&[dir]);
hasher.update(&nc);
hasher.update(b":");
hasher.update(label.as_bytes());
hasher.update(b":");
hasher.update(kind.as_bytes());
hasher.update(b"|");
}
*hasher.finalize().as_bytes()
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use panproto_schema::{EdgeRule, Protocol, SchemaBuilder};
fn proto() -> Protocol {
Protocol {
name: "t".into(),
schema_theory: "ThTest".into(),
instance_theory: "ThWType".into(),
edge_rules: vec![EdgeRule {
edge_kind: "prop".into(),
src_kinds: vec!["object".into(), "record".into()],
tgt_kinds: vec!["string".into(), "object".into()],
}],
obj_kinds: vec!["record".into(), "object".into(), "string".into()],
constraint_sorts: vec![],
..Protocol::default()
}
}
#[test]
fn distinctly_named_isomorphic_neighborhoods_anchor() {
let p = proto();
let src = SchemaBuilder::new(&p)
.vertex("R", "record", None::<&str>)
.unwrap()
.vertex("a", "string", None::<&str>)
.unwrap()
.vertex("b", "string", None::<&str>)
.unwrap()
.edge("R", "a", "prop", Some("first"))
.unwrap()
.edge("R", "b", "prop", Some("second"))
.unwrap()
.build()
.unwrap();
let tgt = SchemaBuilder::new(&p)
.vertex("Z", "record", None::<&str>)
.unwrap()
.vertex("x", "string", None::<&str>)
.unwrap()
.vertex("y", "string", None::<&str>)
.unwrap()
.edge("Z", "x", "prop", Some("first"))
.unwrap()
.edge("Z", "y", "prop", Some("second"))
.unwrap()
.build()
.unwrap();
let anchors = wl_anchors(&src, &tgt, 2);
let pairs: Vec<(&str, &str)> = anchors
.iter()
.map(|a| (a.src.as_str(), a.tgt.as_str()))
.collect();
assert!(
pairs.contains(&("R", "Z")),
"record anchor missing in {pairs:?}"
);
assert!(
pairs.contains(&("a", "x")),
"first child missing: {pairs:?}"
);
assert!(
pairs.contains(&("b", "y")),
"second child missing: {pairs:?}"
);
for anchor in &anchors {
assert_eq!(anchor.strategy, StrategyTag::WlRefinement);
assert!((anchor.confidence - WL_CONFIDENCE).abs() < f64::EPSILON);
}
}
#[test]
fn ambiguous_color_classes_emit_nothing() {
let p = proto();
let src = SchemaBuilder::new(&p)
.vertex("R", "record", None::<&str>)
.unwrap()
.vertex("a", "string", None::<&str>)
.unwrap()
.vertex("b", "string", None::<&str>)
.unwrap()
.edge("R", "a", "prop", Some("x"))
.unwrap()
.edge("R", "b", "prop", Some("x"))
.unwrap()
.build()
.unwrap();
let tgt = SchemaBuilder::new(&p)
.vertex("Z", "record", None::<&str>)
.unwrap()
.vertex("p", "string", None::<&str>)
.unwrap()
.vertex("q", "string", None::<&str>)
.unwrap()
.edge("Z", "p", "prop", Some("x"))
.unwrap()
.edge("Z", "q", "prop", Some("x"))
.unwrap()
.build()
.unwrap();
let anchors = wl_anchors(&src, &tgt, 2);
let pairs: Vec<(&str, &str)> = anchors
.iter()
.map(|a| (a.src.as_str(), a.tgt.as_str()))
.collect();
assert!(pairs.contains(&("R", "Z")));
assert!(!pairs.iter().any(|(s, _)| *s == "a"));
assert!(!pairs.iter().any(|(s, _)| *s == "b"));
}
#[test]
fn zero_iterations_degenerates_to_kind_plus_constraints() {
let p = proto();
let src = SchemaBuilder::new(&p)
.vertex("r", "record", None::<&str>)
.unwrap()
.vertex("a", "string", None::<&str>)
.unwrap()
.edge("r", "a", "prop", Some("x"))
.unwrap()
.build()
.unwrap();
let tgt = SchemaBuilder::new(&p)
.vertex("r2", "record", None::<&str>)
.unwrap()
.vertex("b", "string", None::<&str>)
.unwrap()
.edge("r2", "b", "prop", Some("x"))
.unwrap()
.build()
.unwrap();
let anchors = wl_anchors(&src, &tgt, 0);
assert_eq!(anchors.len(), 2);
}
#[test]
fn deterministic_order() {
let p = proto();
let build = |labels: &[&str]| {
let mut b = SchemaBuilder::new(&p)
.vertex("R", "record", None::<&str>)
.unwrap();
for (i, l) in labels.iter().enumerate() {
let id = format!("v{i}");
b = b.vertex(&id, "string", None::<&str>).unwrap();
b = b.edge("R", &id, "prop", Some(*l)).unwrap();
}
b.build().unwrap()
};
let s = build(&["a", "b", "c"]);
let t = build(&["a", "b", "c"]);
let r1: Vec<_> = wl_anchors(&s, &t, 2)
.iter()
.map(|a| (a.src.as_str().to_owned(), a.tgt.as_str().to_owned()))
.collect();
let r2: Vec<_> = wl_anchors(&s, &t, 2)
.iter()
.map(|a| (a.src.as_str().to_owned(), a.tgt.as_str().to_owned()))
.collect();
assert_eq!(r1, r2);
}
}