use std::collections::HashMap;
use panproto_gat::{Name, Theory};
use panproto_schema::{
Edge, Protocol, Schema, SchemaMorphism, SchemaOverlap, canonical_digest, induce_on_vertices,
schema_pushout,
};
use rustc_hash::FxHashSet;
use crate::error::SpanError;
use crate::existence::{ExistenceReport, check_existence};
use crate::hom_search::{DomainConstraints, FoundMorphism, SearchOptions};
use crate::migration::Migration;
use crate::schema_theory::check_migration_morphism;
use crate::solve::build::{Evidence, NoEvidence, build_cfn, edge_image};
use crate::solve::cost::{COST_SCALE, Cost, CostWeights, DEFAULT_WEIGHTS};
use crate::solve::{
Assignment, Cfn, LimitKind, SearchBudget, SolveOutcome, SolverPath, all_optima, dispatch_plan,
eliminate, solve, solve_iso, solve_monic,
};
const COST_SCALE_FLOAT: f64 = 1.0e9;
pub const DEFAULT_OPTIMA_CAP: usize = 1024;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct SchemaSpan {
pub apex: Schema,
pub left: Migration,
pub right: Migration,
pub quality: f64,
pub quality_bounds: (f64, f64),
pub apex_coverage: f64,
pub certificate: SpanCertificate,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EdgeImages {
Distinct,
#[default]
Shared,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct LegShape {
pub left_is_mono: bool,
pub right_is_mono: bool,
pub right_edge_images: EdgeImages,
pub left_is_iso: bool,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct SpanCertificate {
pub proven_optimal: bool,
pub shape: LegShape,
pub legs_are_functorial: bool,
pub left_existence: ExistenceReport,
pub right_existence: ExistenceReport,
pub apex_pointed: bool,
pub apex_digest: [u8; 32],
pub path: SolverPath,
pub tie_break_order: Option<Vec<Name>>,
pub limit_hit: Option<LimitKind>,
}
impl Default for SpanCertificate {
fn default() -> Self {
Self {
proven_optimal: false,
shape: LegShape::default(),
legs_are_functorial: false,
left_existence: ExistenceReport {
valid: false,
errors: Vec::new(),
},
right_existence: ExistenceReport {
valid: false,
errors: Vec::new(),
},
apex_pointed: false,
apex_digest: [0u8; 32],
path: SolverPath::Eliminate { width: 0 },
tie_break_order: None,
limit_hit: None,
}
}
}
impl SchemaSpan {
#[inline]
#[must_use]
pub const fn is_total(&self) -> bool {
self.certificate.shape.left_is_iso
}
#[must_use]
pub fn as_total_morphism(&self) -> Option<FoundMorphism> {
self.is_total().then(|| FoundMorphism {
vertex_map: self.right.vertex_map.clone(),
edge_map: self.right.edge_map.clone(),
quality: self.quality,
})
}
#[must_use]
pub fn to_overlap(&self) -> SchemaOverlap {
let mut vertex_pairs: Vec<(Name, Name)> = self
.right
.vertex_map
.iter()
.map(|(source, image)| (source.clone(), image.clone()))
.collect();
vertex_pairs.sort_unstable();
let mut edge_pairs: Vec<(Edge, Edge)> = self
.right
.edge_map
.iter()
.map(|(source, image)| (source.clone(), image.clone()))
.collect();
edge_pairs.sort_unstable();
SchemaOverlap {
vertex_pairs,
edge_pairs,
}
}
pub fn pushout(
&self,
src: &Schema,
tgt: &Schema,
) -> Result<(Schema, SchemaMorphism, SchemaMorphism), SpanError> {
if !self.certificate.shape.right_is_mono {
return Err(SpanError::ContractingRightLeg);
}
Ok(schema_pushout(src, tgt, &self.to_overlap())?)
}
#[must_use]
pub fn apex_digest_hex(&self) -> String {
digest_hex(&self.certificate.apex_digest)
}
}
pub struct SpanSearch<'a> {
protocol: &'a Protocol,
options: SearchOptions,
constraints: DomainConstraints,
evidence: &'a dyn Evidence,
weights: CostWeights,
budget: SearchBudget,
theories: Option<&'a HashMap<String, Theory>>,
}
impl std::fmt::Debug for SpanSearch<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SpanSearch")
.field("protocol", &self.protocol.name)
.field("options", &self.options)
.field("constraints", &self.constraints)
.field("weights", &self.weights)
.field("budget", &self.budget)
.field("theories", &self.theories.map_or(0, HashMap::len))
.finish()
}
}
impl<'a> SpanSearch<'a> {
#[must_use]
pub fn new(protocol: &'a Protocol) -> Self {
Self {
protocol,
options: SearchOptions::default(),
constraints: DomainConstraints::default(),
evidence: &NoEvidence,
weights: DEFAULT_WEIGHTS,
budget: SearchBudget::default(),
theories: None,
}
}
#[must_use]
pub fn with_options(mut self, options: SearchOptions) -> Self {
self.options = options;
self
}
#[must_use]
pub fn with_constraints(mut self, constraints: DomainConstraints) -> Self {
self.constraints = constraints;
self
}
#[must_use]
pub fn with_evidence(mut self, evidence: &'a dyn Evidence) -> Self {
self.evidence = evidence;
self
}
#[must_use]
pub const fn with_weights(mut self, weights: CostWeights) -> Self {
self.weights = weights;
self
}
#[must_use]
pub const fn with_budget(mut self, budget: SearchBudget) -> Self {
self.budget = budget;
self
}
#[must_use]
pub const fn with_theories(mut self, theories: &'a HashMap<String, Theory>) -> Self {
self.theories = Some(theories);
self
}
pub fn run(&self, src: &Schema, tgt: &Schema) -> Result<SchemaSpan, SpanError> {
let cfn = self.network(src, tgt)?;
let outcome = self.dispatch(&cfn, src, tgt)?;
let assignment = outcome
.best
.clone()
.unwrap_or_else(|| Assignment::all_bottom(cfn.n_variables()));
self.assemble(src, tgt, &cfn, &outcome, &assignment)
}
pub fn optima(
&self,
src: &Schema,
tgt: &Schema,
limit: usize,
) -> Result<Vec<SchemaSpan>, SpanError> {
let limit = if limit == 0 {
DEFAULT_OPTIMA_CAP
} else {
limit
};
let cfn = self.network(src, tgt)?;
let plan = dispatch_plan(&cfn, &self.budget);
if self.options.iso || self.options.monic || !plan.exact {
return self.run(src, tgt).map(|span| vec![span]);
}
let buckets = eliminate(&cfn, &plan.order);
let optimum = buckets.optimum();
let outcome = SolveOutcome {
best: None,
lower_bound: optimum,
upper_bound: optimum,
proven_optimal: true,
path: SolverPath::Eliminate { width: plan.width },
elimination_order: Some(plan.order),
nodes: 0,
limit_hit: None,
warnings: Vec::new(),
};
all_optima(&cfn, &buckets, limit)
.iter()
.map(|assignment| self.assemble(src, tgt, &cfn, &outcome, assignment))
.collect()
}
fn network(&self, src: &Schema, tgt: &Schema) -> Result<Cfn, SpanError> {
if self.options.epic {
return Err(SpanError::EpicIsNotASpanProperty);
}
Ok(build_cfn(
src,
tgt,
&self.options,
&self.constraints,
self.evidence,
self.constraints.scoring_weights.unwrap_or(self.weights),
self.budget.mem_bytes,
)?)
}
fn dispatch(&self, cfn: &Cfn, src: &Schema, tgt: &Schema) -> Result<SolveOutcome, SpanError> {
if self.options.iso {
return Ok(solve_iso(cfn, src, tgt, &self.budget)?);
}
if self.options.monic {
return Ok(solve_monic(cfn, &self.budget));
}
Ok(solve(cfn, &self.budget))
}
fn assemble(
&self,
src: &Schema,
tgt: &Schema,
cfn: &Cfn,
outcome: &SolveOutcome,
assignment: &Assignment,
) -> Result<SchemaSpan, SpanError> {
let images = image_map(cfn, assignment);
let keep_v: FxHashSet<Name> = images.keys().cloned().collect();
let apex = induce_on_vertices(src, self.protocol, &keep_v)?;
debug_assert_apex_lost_nothing(src, &apex, &keep_v);
let apex_digest = canonical_digest(&apex);
let apex_id = Name::from(digest_hex(&apex_digest).as_str());
let src_id = Name::from(digest_hex(&canonical_digest(src)).as_str());
let tgt_id = Name::from(digest_hex(&canonical_digest(tgt)).as_str());
let left = inclusion(&apex, apex_id.clone(), src_id);
let right = right_leg(&apex, tgt, &images, self.options.iso)
.with_endpoints(Some(apex_id), Some(tgt_id));
let radix = cfn.radix();
let certificate = SpanCertificate {
proven_optimal: outcome.proven_optimal,
shape: LegShape {
left_is_mono: is_injective(&left),
right_is_mono: is_vertex_injective(&right),
right_edge_images: if is_edge_injective(&right) {
EdgeImages::Distinct
} else {
EdgeImages::Shared
},
left_is_iso: apex.vertices.len() == src.vertices.len()
&& apex.edges.len() == mappable_edges(src),
},
legs_are_functorial: legs_are_functorial(&apex, src, tgt, &left, &right),
left_existence: self.existence_of(&apex, src, &left),
right_existence: self.existence_of(&apex, tgt, &right),
apex_pointed: !apex.entries.is_empty(),
apex_digest,
path: outcome.path,
tie_break_order: decode_order(cfn, outcome),
limit_hit: outcome.limit_hit,
};
Ok(SchemaSpan {
apex_coverage: coverage(&apex, src),
apex,
left,
right,
quality: cfn.quality_of(assignment),
quality_bounds: (
quality_of_cost(outcome.upper_bound, radix),
quality_of_cost(outcome.lower_bound, radix),
),
certificate,
})
}
fn existence_of(&self, apex: &Schema, codomain: &Schema, leg: &Migration) -> ExistenceReport {
let empty = HashMap::new();
let theories = self.theories.unwrap_or(&empty);
check_existence(self.protocol, apex, codomain, leg, theories)
}
}
pub(crate) fn mappable_edges(src: &Schema) -> usize {
src.edges
.keys()
.filter(|edge| src.vertices.contains_key(&edge.src) && src.vertices.contains_key(&edge.tgt))
.count()
}
fn decode_order(cfn: &Cfn, outcome: &SolveOutcome) -> Option<Vec<Name>> {
let order = outcome.elimination_order.as_ref()?;
Some(
order
.iter()
.rev()
.filter_map(|var| Some(cfn.variable(*var)?.name().clone()))
.collect(),
)
}
pub(crate) fn image_map(cfn: &Cfn, assignment: &Assignment) -> HashMap<Name, Name> {
let mut images = HashMap::new();
for (var, value) in assignment.pairs() {
let Some(variable) = cfn.variable(var) else {
continue;
};
let Some(image) = variable.value_name(value) else {
continue;
};
images.insert(variable.name().clone(), image.clone());
}
images
}
fn inclusion(apex: &Schema, apex_id: Name, src_id: Name) -> Migration {
let vertices: Vec<Name> = apex.vertices.keys().cloned().collect();
let edges: Vec<Edge> = apex.edges.keys().cloned().collect();
Migration::identity(&vertices, &edges).with_endpoints(Some(apex_id), Some(src_id))
}
fn right_leg(
apex: &Schema,
tgt: &Schema,
images: &HashMap<Name, Name>,
bijective_edges: bool,
) -> Migration {
let vertex_map: HashMap<Name, Name> = apex
.vertices
.keys()
.filter_map(|vertex| Some((vertex.clone(), images.get(vertex)?.clone())))
.collect();
let edge_map = if bijective_edges {
bijective_edge_map(apex, tgt, &vertex_map)
.unwrap_or_else(|| greedy_edge_map(apex, tgt, &vertex_map))
} else {
greedy_edge_map(apex, tgt, &vertex_map)
};
Migration {
vertex_map,
edge_map,
hyper_edge_map: HashMap::new(),
label_map: HashMap::new(),
resolver: HashMap::new(),
hyper_resolver: HashMap::new(),
expr_resolvers: HashMap::new(),
coercions: HashMap::new(),
domain: None,
codomain: None,
}
}
pub(crate) fn greedy_edge_map(
apex: &Schema,
tgt: &Schema,
vertex_map: &HashMap<Name, Name>,
) -> HashMap<Edge, Edge> {
let mut edge_map = HashMap::new();
for edge in apex.edges.keys() {
let (Some(from), Some(to)) = (vertex_map.get(&edge.src), vertex_map.get(&edge.tgt)) else {
continue;
};
if let Some(image) = edge_image(tgt, edge, from, to) {
edge_map.insert(edge.clone(), image.clone());
}
}
edge_map
}
pub(crate) fn bijective_edge_map(
apex: &Schema,
tgt: &Schema,
vertex_map: &HashMap<Name, Name>,
) -> Option<HashMap<Edge, Edge>> {
let mut groups: std::collections::BTreeMap<(Name, Name), Vec<&Edge>> =
std::collections::BTreeMap::new();
for edge in apex.edges.keys() {
let from = vertex_map.get(&edge.src)?;
let to = vertex_map.get(&edge.tgt)?;
groups
.entry((from.clone(), to.clone()))
.or_default()
.push(edge);
}
let mut edge_map: HashMap<Edge, Edge> = HashMap::new();
let mut covered = 0usize;
for ((from, to), mut sources) in groups {
sources.sort_unstable();
let pool = tgt.edges_between(from.as_str(), to.as_str());
if sources.len() != pool.len() {
return None;
}
covered += pool.len();
let mut taken = vec![false; pool.len()];
for edge in sources {
let named = pool.iter().enumerate().find_map(|(slot, candidate)| {
let free = taken.get(slot).copied() == Some(false);
(free && candidate.kind == edge.kind && candidate.name == edge.name).then_some(slot)
});
let slot = named.or_else(|| {
pool.iter().enumerate().find_map(|(slot, candidate)| {
let free = taken.get(slot).copied() == Some(false);
(free && candidate.kind == edge.kind).then_some(slot)
})
})?;
*taken.get_mut(slot)? = true;
edge_map.insert(edge.clone(), pool.get(slot)?.clone());
}
}
let images: std::collections::BTreeSet<&Name> = vertex_map.values().collect();
let reachable = tgt
.edges
.keys()
.filter(|edge| images.contains(&edge.src) && images.contains(&edge.tgt))
.count();
if covered != reachable {
return None;
}
Some(edge_map)
}
fn is_injective(migration: &Migration) -> bool {
is_vertex_injective(migration) && is_edge_injective(migration)
}
fn is_edge_injective(migration: &Migration) -> bool {
let mut images: Vec<&Edge> = migration.edge_map.values().collect();
images.sort_unstable();
let before = images.len();
images.dedup();
images.len() == before
}
fn is_vertex_injective(migration: &Migration) -> bool {
let mut images: Vec<&Name> = migration.vertex_map.values().collect();
images.sort_unstable();
let before = images.len();
images.dedup();
images.len() == before
}
fn legs_are_functorial(
apex: &Schema,
src: &Schema,
tgt: &Schema,
left: &Migration,
right: &Migration,
) -> bool {
let left_ok = check_migration_morphism(apex, src, left).is_ok();
debug_assert!(
left_ok,
"the left leg is an inclusion, so it cannot fail to be a morphism"
);
left_ok && check_migration_morphism(apex, tgt, right).is_ok()
}
fn coverage(apex: &Schema, src: &Schema) -> f64 {
if src.vertices.is_empty() {
return 1.0;
}
let kept = u32::try_from(apex.vertices.len()).unwrap_or(u32::MAX);
let total = u32::try_from(src.vertices.len()).unwrap_or(u32::MAX);
f64::from(kept) / f64::from(total)
}
fn quality_of_cost(cost: Cost, radix: u64) -> f64 {
let units = cost.quality_part(radix).min(COST_SCALE);
let units = u32::try_from(units).unwrap_or(u32::MAX);
1.0 - f64::from(units) / COST_SCALE_FLOAT
}
fn digest_hex(digest: &[u8; 32]) -> String {
blake3::Hash::from_bytes(*digest).to_hex().to_string()
}
fn debug_assert_apex_lost_nothing(src: &Schema, apex: &Schema, keep_v: &FxHashSet<Name>) {
debug_assert!(
signatures_survived_whole(src, keep_v),
"a hyper-edge signature survived in part, so its clique constraint is missing"
);
debug_assert!(
fixpoints_kept_their_targets(src, keep_v),
"a fixpoint marker survived without its target, so its constraint is missing"
);
debug_assert!(
schema_spans_survived_whole(src, keep_v),
"a schema span survived at one end only, so its constraint is missing"
);
debug_assert!(
coproducts_kept_their_arms(src, apex, keep_v),
"a coproduct survived without one of its arms, so its constraint is missing"
);
}
fn holds(src: &Schema, vertex: &Name) -> bool {
src.vertices.contains_key(vertex)
}
fn signatures_survived_whole(src: &Schema, keep_v: &FxHashSet<Name>) -> bool {
src.hyper_edges.values().all(|hyper| {
let members = || hyper.signature.values().filter(|v| holds(src, v));
members().all(|v| keep_v.contains(v)) || members().all(|v| !keep_v.contains(v))
})
}
fn fixpoints_kept_their_targets(src: &Schema, keep_v: &FxHashSet<Name>) -> bool {
src.recursion_points.iter().all(|(mu, point)| {
!keep_v.contains(mu)
|| !holds(src, &point.target_vertex)
|| keep_v.contains(&point.target_vertex)
})
}
fn schema_spans_survived_whole(src: &Schema, keep_v: &FxHashSet<Name>) -> bool {
src.spans.values().all(|span| {
!holds(src, &span.left)
|| !holds(src, &span.right)
|| keep_v.contains(&span.left) == keep_v.contains(&span.right)
})
}
fn coproducts_kept_their_arms(src: &Schema, apex: &Schema, keep_v: &FxHashSet<Name>) -> bool {
src.variants
.iter()
.filter(|(coproduct, _)| keep_v.contains(*coproduct))
.all(|(coproduct, arms)| {
let whole = arms
.iter()
.filter(|arm| holds(src, &arm.id) && holds(src, &arm.parent_vertex))
.count();
arms.iter().all(|arm| {
(!holds(src, &arm.id) || keep_v.contains(&arm.id))
&& (!holds(src, &arm.parent_vertex) || keep_v.contains(&arm.parent_vertex))
}) && apex.variants.get(coproduct).map_or(0, Vec::len) == whole
})
}
#[cfg(test)]
pub(crate) fn empty_schema(protocol: &str) -> Schema {
Schema {
protocol: protocol.to_owned(),
vertices: HashMap::new(),
edges: HashMap::new(),
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
between: HashMap::new(),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::float_cmp,
clippy::too_many_lines
)]
mod tests {
use super::*;
use panproto_schema::{SchemaBuilder, validate};
fn protocol() -> Protocol {
Protocol {
name: "test".into(),
schema_theory: "ThTest".into(),
instance_theory: "ThWType".into(),
edge_rules: vec![],
obj_kinds: vec!["object".into(), "string".into(), "integer".into()],
constraint_sorts: vec![],
..Protocol::default()
}
}
fn schema(vertices: &[(&str, &str)], edges: &[(&str, &str, &str, &str)]) -> Schema {
let protocol = protocol();
let mut builder = SchemaBuilder::new(&protocol);
for (id, kind) in vertices {
builder = builder.vertex(id, kind, None::<&str>).unwrap();
}
for (from, to, kind, name) in edges {
builder = builder.edge(from, to, kind, Some(*name)).unwrap();
}
if let Some((entry, _)) = vertices.first() {
builder = builder.entry(entry);
}
builder.build().unwrap()
}
#[test]
fn the_cost_scale_float_is_the_cost_scale() {
assert_eq!(COST_SCALE, 1_000_000_000);
assert!((COST_SCALE_FLOAT - 1_000_000_000.0).abs() < f64::EPSILON);
}
#[test]
fn a_schema_against_itself_is_a_total_morphism() {
let s = schema(
&[("root", "object"), ("root.name", "string")],
&[("root", "root.name", "prop", "name")],
);
let protocol = protocol();
let span = SpanSearch::new(&protocol).run(&s, &s).unwrap();
assert!(span.is_total(), "a schema maps onto itself");
assert!(span.certificate.shape.left_is_iso);
assert_eq!(span.apex_coverage, 1.0);
assert_eq!(span.apex.vertices.len(), s.vertices.len());
assert_eq!(span.apex.edges.len(), s.edges.len());
let total = span.as_total_morphism().expect("a total span has a shape");
assert_eq!(total.vertex_map, span.right.vertex_map);
assert_eq!(total.edge_map, span.right.edge_map);
assert_eq!(total.quality, span.quality);
for (source, image) in &total.vertex_map {
assert_eq!(source, image, "the identity is the optimum here");
}
}
#[test]
fn the_left_leg_is_an_inclusion_and_the_legs_carry_endpoints() {
let s = schema(
&[("root", "object"), ("root.name", "string")],
&[("root", "root.name", "prop", "name")],
);
let protocol = protocol();
let span = SpanSearch::new(&protocol).run(&s, &s).unwrap();
for (source, image) in &span.left.vertex_map {
assert_eq!(source, image, "the left leg is the identity on vertices");
}
for (source, image) in &span.left.edge_map {
assert_eq!(source, image, "the left leg is the identity on edges");
}
assert!(span.certificate.shape.left_is_mono);
assert_eq!(
span.left.domain.as_ref().map(Name::as_str),
Some(span.apex_digest_hex().as_str()),
"both legs leave the apex"
);
assert_eq!(span.left.domain, span.right.domain);
assert!(span.left.codomain.is_some());
assert!(span.right.codomain.is_some());
}
#[test]
fn both_legs_pass_their_checks() {
let src = schema(
&[("root", "object"), ("root.text", "string")],
&[("root", "root.text", "prop", "text")],
);
let tgt = schema(
&[("root", "object"), ("root.body", "string")],
&[("root", "root.body", "prop", "body")],
);
let protocol = protocol();
let span = SpanSearch::new(&protocol).run(&src, &tgt).unwrap();
assert!(span.certificate.legs_are_functorial);
assert!(
span.certificate.right_existence.valid,
"existence reported {:?}",
span.certificate.right_existence.errors
);
assert!(check_migration_morphism(&span.apex, &src, &span.left).is_ok());
assert!(check_migration_morphism(&span.apex, &tgt, &span.right).is_ok());
}
#[test]
fn a_pair_with_no_shared_kinds_returns_an_empty_apex_without_error() {
let src = schema(
&[("a", "object"), ("a.x", "string")],
&[("a", "a.x", "prop", "x")],
);
let tgt = schema(&[("b", "integer"), ("c", "integer")], &[]);
let protocol = protocol();
let span = SpanSearch::new(&protocol).run(&src, &tgt).unwrap();
assert!(span.apex.vertices.is_empty(), "nothing can be matched");
assert!(span.apex.edges.is_empty());
assert_eq!(span.apex_coverage, 0.0);
assert!(!span.is_total());
assert!(span.as_total_morphism().is_none());
assert!(span.to_overlap().vertex_pairs.is_empty());
}
#[test]
fn an_empty_source_returns_a_span_rather_than_an_error() {
let empty = super::empty_schema("test");
let tgt = schema(&[("root", "object")], &[]);
let protocol = protocol();
let span = SpanSearch::new(&protocol).run(&empty, &tgt).unwrap();
assert!(span.apex.vertices.is_empty());
assert_eq!(span.apex_coverage, 1.0, "nothing to cover is fully covered");
assert!(span.is_total(), "the empty inclusion is onto");
assert!(!span.certificate.apex_pointed);
}
#[test]
fn the_apex_validates_and_its_digest_is_stable() {
let src = schema(
&[
("root", "object"),
("root.a", "string"),
("root.n", "integer"),
],
&[
("root", "root.a", "prop", "a"),
("root", "root.n", "prop", "n"),
],
);
let tgt = schema(
&[("root", "object"), ("root.a", "string")],
&[("root", "root.a", "prop", "a")],
);
let protocol = protocol();
let search = SpanSearch::new(&protocol);
let first = search.run(&src, &tgt).unwrap();
let second = search.run(&src, &tgt).unwrap();
assert_eq!(
first.certificate.apex_digest,
second.certificate.apex_digest
);
assert_eq!(first.apex_digest_hex(), second.apex_digest_hex());
assert_eq!(first.apex_digest_hex().len(), 64);
assert!(validate(&first.apex, &protocol).is_empty());
assert_eq!(
canonical_digest(&first.apex),
first.certificate.apex_digest,
"the digest is the apex's own"
);
}
#[test]
fn the_apex_keeps_its_adjacency_indexes() {
let s = schema(
&[("root", "object"), ("root.a", "string")],
&[("root", "root.a", "prop", "a")],
);
let protocol = protocol();
let span = SpanSearch::new(&protocol).run(&s, &s).unwrap();
assert!(!span.apex.edges.is_empty());
assert!(!span.apex.outgoing_edges("root").is_empty());
assert!(!span.apex.incoming_edges("root.a").is_empty());
assert!(!span.apex.edges_between("root", "root.a").is_empty());
}
#[test]
fn to_overlap_and_pushout_succeed() {
let src = schema(
&[("root", "object"), ("root.text", "string")],
&[("root", "root.text", "prop", "text")],
);
let tgt = schema(
&[("root", "object"), ("root.body", "string")],
&[("root", "root.body", "prop", "body")],
);
let protocol = protocol();
let span = SpanSearch::new(&protocol).run(&src, &tgt).unwrap();
let overlap = span.to_overlap();
assert_eq!(overlap.vertex_pairs.len(), span.apex.vertices.len());
assert_eq!(overlap.edge_pairs.len(), span.right.edge_map.len());
let sorted = {
let mut pairs = overlap.vertex_pairs.clone();
pairs.sort_unstable();
pairs
};
assert_eq!(overlap.vertex_pairs, sorted, "the pair list is ordered");
let (merged, left, right) = span.pushout(&src, &tgt).unwrap();
assert!(!merged.vertices.is_empty());
assert_eq!(left.vertex_map.len(), src.vertices.len());
assert_eq!(right.vertex_map.len(), tgt.vertices.len());
}
#[test]
fn iso_makes_the_right_leg_a_mono() {
let src = schema(
&[("a", "object"), ("b", "string")],
&[("a", "b", "prop", "p")],
);
let tgt = schema(
&[("x", "object"), ("y", "string")],
&[("x", "y", "prop", "q")],
);
let protocol = protocol();
let options = SearchOptions {
iso: true,
..SearchOptions::default()
};
let span = SpanSearch::new(&protocol)
.with_options(options)
.run(&src, &tgt)
.unwrap();
assert!(span.certificate.shape.right_is_mono);
assert_eq!(span.certificate.path, SolverPath::Iso);
assert_eq!(
span.apex.vertices.len(),
2,
"the two schemas are isomorphic"
);
assert_eq!(span.right.edge_map.len(), 1);
}
#[test]
fn a_partial_match_drops_only_what_it_must() {
let src = schema(
&[
("root", "object"),
("root.a", "string"),
("root.n", "integer"),
],
&[
("root", "root.a", "prop", "a"),
("root", "root.n", "prop", "n"),
],
);
let tgt = schema(
&[("root", "object"), ("root.a", "string")],
&[("root", "root.a", "prop", "a")],
);
let protocol = protocol();
let span = SpanSearch::new(&protocol).run(&src, &tgt).unwrap();
assert!(!span.is_total());
assert_eq!(span.apex.vertices.len(), 2, "only the integer is dropped");
assert!(span.apex.vertices.contains_key("root"));
assert!(span.apex.vertices.contains_key("root.a"));
assert!(!span.apex.vertices.contains_key("root.n"));
assert!((span.apex_coverage - 2.0 / 3.0).abs() < 1e-12);
assert_eq!(span.apex.edges.len(), 1, "the dropped edge went with it");
}
#[test]
fn the_apex_keeps_its_non_edge_structure_whole() {
let mut src = schema(
&[
("root", "object"),
("shape", "object"),
("shape.circle", "string"),
("shape.square", "string"),
("mu", "object"),
("orphan", "integer"),
],
&[
("root", "shape", "prop", "shape"),
("root", "mu", "prop", "mu"),
("root", "orphan", "prop", "orphan"),
],
);
src.variants.insert(
Name::from("shape"),
vec![
panproto_schema::Variant {
id: Name::from("shape.circle"),
parent_vertex: Name::from("shape"),
tag: None,
},
panproto_schema::Variant {
id: Name::from("shape.square"),
parent_vertex: Name::from("shape"),
tag: None,
},
],
);
src.recursion_points.insert(
Name::from("mu"),
panproto_schema::RecursionPoint {
target_vertex: Name::from("root"),
},
);
src.spans.insert(
Name::from("s"),
panproto_schema::Span {
id: Name::from("s"),
left: Name::from("shape.circle"),
right: Name::from("shape.square"),
},
);
src.hyper_edges.insert(
Name::from("h"),
panproto_schema::HyperEdge {
id: Name::from("h"),
kind: Name::from("record"),
signature: HashMap::from([
(Name::from("l"), Name::from("shape.circle")),
(Name::from("r"), Name::from("shape.square")),
]),
parent_label: Name::from("l"),
},
);
let tgt = schema(
&[
("root", "object"),
("shape", "object"),
("shape.circle", "string"),
("mu", "object"),
],
&[
("root", "shape", "prop", "shape"),
("root", "mu", "prop", "mu"),
("shape", "shape.circle", "variant", "circle"),
],
);
let protocol = protocol();
let opts = SearchOptions {
monic: true,
..SearchOptions::default()
};
let span = SpanSearch::new(&protocol)
.with_options(opts)
.run(&src, &tgt)
.unwrap();
let kept = |id: &str| span.apex.vertices.contains_key(id);
assert!(!kept("shape.circle"), "the pair is dropped whole");
assert!(!kept("shape.square"));
assert!(!kept("shape"), "and the coproduct with it");
assert!(span.apex.variants.is_empty());
assert!(span.apex.spans.is_empty());
assert!(span.apex.hyper_edges.is_empty());
assert!(!kept("orphan"), "the integer has nowhere to go");
assert!(kept("root") && kept("mu"), "the rest survives");
assert_eq!(
span.apex.recursion_points.len(),
1,
"the marker and its target both survive, so the point does"
);
assert!(validate(&span.apex, &protocol).is_empty());
let wide = schema(
&[
("root", "object"),
("shape", "object"),
("shape.circle", "string"),
("shape.square", "string"),
("mu", "object"),
],
&[
("root", "shape", "prop", "shape"),
("root", "mu", "prop", "mu"),
("shape", "shape.circle", "variant", "circle"),
("shape", "shape.square", "variant", "square"),
],
);
let span = SpanSearch::new(&protocol)
.with_options(SearchOptions {
monic: true,
..SearchOptions::default()
})
.run(&src, &wide)
.unwrap();
assert!(span.apex.vertices.contains_key("shape"));
assert_eq!(
span.apex.variants.get("shape").map_or(0, Vec::len),
2,
"a surviving coproduct keeps every arm"
);
assert_eq!(span.apex.spans.len(), 1);
assert_eq!(span.apex.hyper_edges.len(), 1);
assert!(!span.apex.vertices.contains_key("orphan"));
assert!(validate(&span.apex, &protocol).is_empty());
}
#[test]
fn the_quality_bounds_bracket_the_quality() {
let src = schema(
&[("root", "object"), ("root.a", "string")],
&[("root", "root.a", "prop", "a")],
);
let tgt = schema(
&[("root", "object"), ("root.b", "string")],
&[("root", "root.b", "prop", "b")],
);
let protocol = protocol();
let span = SpanSearch::new(&protocol).run(&src, &tgt).unwrap();
let (low, high) = span.quality_bounds;
assert!(low <= span.quality && span.quality <= high);
assert!(span.certificate.proven_optimal);
assert_eq!(low, high, "a proven optimum has no interval");
assert!((0.0..=1.0).contains(&span.quality));
}
#[test]
fn optima_agree_with_the_single_answer() {
let src = schema(
&[("root", "object"), ("root.a", "string")],
&[("root", "root.a", "prop", "a")],
);
let tgt = schema(
&[
("root", "object"),
("root.x", "string"),
("root.y", "string"),
],
&[
("root", "root.x", "prop", "a"),
("root", "root.y", "prop", "a"),
],
);
let protocol = protocol();
let search = SpanSearch::new(&protocol);
let one = search.run(&src, &tgt).unwrap();
let all = search.optima(&src, &tgt, 8).unwrap();
assert!(!all.is_empty());
for span in &all {
assert_eq!(
span.quality, one.quality,
"every enumerated span attains the optimum"
);
}
assert!(
all.iter()
.any(|span| span.right.vertex_map == one.right.vertex_map),
"the canonical answer is among the optima"
);
}
#[test]
fn the_default_certificate_establishes_nothing() {
let certificate = SpanCertificate::default();
assert!(!certificate.proven_optimal);
assert!(!certificate.shape.left_is_mono);
assert!(!certificate.right_existence.valid);
assert_eq!(certificate.apex_digest, [0u8; 32]);
}
}