use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use panproto_gat::{Name, Operation, Sort, Theory, TheoryEndofunctor, TheoryTransform};
use panproto_inst::CompiledMigration;
use panproto_inst::value::Value;
use panproto_schema::{Edge, Protocol, Schema, Vertex};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use crate::Lens;
use crate::error::LensError;
#[inline]
fn name_arc_clone(n: &Name) -> Arc<str> {
Arc::clone(&n.0)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ComplementConstructor {
Empty,
DroppedSortData {
sort: Name,
},
DroppedOpData {
op: Name,
},
DroppedEdge {
src: Name,
tgt: Name,
edge_name: Option<Name>,
edge_kind: Name,
},
NatTransKernel {
nat_trans_name: Name,
},
AddedElement {
element_name: Name,
element_kind: String,
default_value: Option<Value>,
},
CoercedSortData {
sort: Name,
class: panproto_gat::CoercionClass,
},
Composite(Vec<Self>),
Scoped {
focus: Name,
inner: Box<Self>,
},
Enrichment {
kind: panproto_gat::EnrichmentKind,
enricher: Arc<str>,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Protolens {
pub name: Name,
pub source: TheoryEndofunctor,
pub target: TheoryEndofunctor,
pub complement_constructor: ComplementConstructor,
}
impl Protolens {
#[must_use]
pub fn applicable_to(&self, schema: &Schema) -> bool {
self.check_applicability(schema).is_ok()
}
pub fn check_applicability(&self, schema: &Schema) -> Result<(), Vec<String>> {
let constraint = SchemaConstraint::from_theory_constraint(&self.source.precondition);
let reasons = constraint.check(schema);
if reasons.is_empty() {
Ok(())
} else {
Err(reasons)
}
}
pub fn instantiate(&self, schema: &Schema, protocol: &Protocol) -> Result<Lens, LensError> {
let src_schema = if matches!(self.source.transform, TheoryTransform::Identity) {
schema.clone()
} else {
apply_theory_transform_to_schema(&self.source.transform, schema, protocol)?
};
let tgt_schema =
apply_theory_transform_to_schema(&self.target.transform, schema, protocol)?;
let compiled = compute_migration_between(&src_schema, &tgt_schema);
Ok(Lens {
compiled,
src_schema,
tgt_schema,
})
}
pub fn instantiate_edit(
&self,
schema: &Schema,
protocol: &Protocol,
) -> Result<crate::EditLens, LensError> {
let base_lens = self.instantiate(schema, protocol)?;
Ok(crate::EditLens::from_lens(base_lens, protocol.clone()))
}
pub fn target_schema(&self, schema: &Schema, protocol: &Protocol) -> Result<Schema, LensError> {
apply_theory_transform_to_schema(&self.target.transform, schema, protocol)
}
#[must_use]
pub fn optic_kind(&self) -> crate::optic::OpticKind {
crate::optic::classify_transform(&self.target.transform)
}
#[must_use]
pub const fn is_lossless(&self) -> bool {
matches!(
self.complement_constructor,
ComplementConstructor::Empty
| ComplementConstructor::CoercedSortData {
class: panproto_gat::CoercionClass::Iso,
..
}
)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
impl ProtolensChain {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum SchemaConstraint {
Unconstrained,
HasVertexKind(Name),
HasVertex(Name),
HasEdgeKind(Name),
HasEdgeBetween {
src: Name,
tgt: Name,
},
Theory(panproto_gat::TheoryConstraint),
All(Vec<Self>),
Any(Vec<Self>),
Not(Box<Self>),
}
impl SchemaConstraint {
#[must_use]
pub fn satisfied_by(&self, schema: &Schema) -> bool {
match self {
Self::Unconstrained => true,
Self::HasVertexKind(kind) => schema.vertices.values().any(|v| v.kind == *kind),
Self::HasVertex(name) => schema.vertices.contains_key(name),
Self::HasEdgeKind(kind) => schema.edges.keys().any(|e| e.kind == *kind),
Self::HasEdgeBetween { src, tgt } => {
schema.edges.keys().any(|e| e.src == *src && e.tgt == *tgt)
}
Self::Theory(tc) => {
let implicit = schema_to_implicit_theory(schema);
tc.satisfied_by(&implicit)
}
Self::All(cs) => cs.iter().all(|c| c.satisfied_by(schema)),
Self::Any(cs) => cs.iter().any(|c| c.satisfied_by(schema)),
Self::Not(c) => !c.satisfied_by(schema),
}
}
#[must_use]
pub fn check(&self, schema: &Schema) -> Vec<String> {
match self {
Self::Unconstrained => vec![],
Self::HasVertexKind(kind) => {
if schema.vertices.values().any(|v| v.kind == *kind) {
vec![]
} else {
vec![format!("Schema has no vertex of kind '{kind}'.")]
}
}
Self::HasVertex(name) => {
if schema.vertices.contains_key(name) {
vec![]
} else {
vec![format!("Schema has no vertex named '{name}'.")]
}
}
Self::HasEdgeKind(kind) => {
if schema.edges.keys().any(|e| e.kind == *kind) {
vec![]
} else {
vec![format!("Schema has no edge of kind '{kind}'.")]
}
}
Self::HasEdgeBetween { src, tgt } => {
if schema.edges.keys().any(|e| e.src == *src && e.tgt == *tgt) {
vec![]
} else {
vec![format!("Schema has no edge from '{src}' to '{tgt}'.")]
}
}
Self::Theory(tc) => {
let implicit = schema_to_implicit_theory(schema);
if tc.satisfied_by(&implicit) {
vec![]
} else {
vec![format!("TheoryConstraint not satisfied: {tc:?}")]
}
}
Self::All(cs) => cs.iter().flat_map(|c| c.check(schema)).collect(),
Self::Any(cs) => {
if cs.iter().any(|c| c.satisfied_by(schema)) {
vec![]
} else {
let reasons: Vec<String> = cs.iter().flat_map(|c| c.check(schema)).collect();
vec![format!(
"None of the alternatives were satisfied: {}",
reasons.join("; ")
)]
}
}
Self::Not(c) => {
if c.satisfied_by(schema) {
vec![format!("Constraint should NOT be satisfied but is: {c:?}")]
} else {
vec![]
}
}
}
}
#[must_use]
pub fn from_theory_constraint(tc: &panproto_gat::TheoryConstraint) -> Self {
match tc {
panproto_gat::TheoryConstraint::Unconstrained => Self::Unconstrained,
panproto_gat::TheoryConstraint::HasSort(name) => {
Self::HasVertexKind(Name::from(&**name))
}
panproto_gat::TheoryConstraint::HasOp(name) => Self::HasEdgeKind(Name::from(&**name)),
panproto_gat::TheoryConstraint::HasEquation(name) => Self::Theory(
panproto_gat::TheoryConstraint::HasEquation(Arc::clone(name)),
),
panproto_gat::TheoryConstraint::All(cs) => {
Self::All(cs.iter().map(Self::from_theory_constraint).collect())
}
panproto_gat::TheoryConstraint::Any(cs) => {
Self::Any(cs.iter().map(Self::from_theory_constraint).collect())
}
panproto_gat::TheoryConstraint::Not(c) => {
Self::Not(Box::new(Self::from_theory_constraint(c)))
}
other @ (panproto_gat::TheoryConstraint::HasDirectedEq(_)
| panproto_gat::TheoryConstraint::HasValSort(_)
| panproto_gat::TheoryConstraint::HasCoercion { .. }
| panproto_gat::TheoryConstraint::HasMerger(_)
| panproto_gat::TheoryConstraint::HasPolicy(_)) => Self::Theory(other.clone()),
}
}
}
#[must_use]
pub fn theory_endofunctor_equiv(a: &TheoryEndofunctor, b: &TheoryEndofunctor) -> bool {
a.precondition == b.precondition && a.transform == b.transform
}
#[must_use]
pub fn protolens_composable(eta: &Protolens, theta: &Protolens) -> bool {
matches!(theta.source.transform, TheoryTransform::Identity)
|| theory_endofunctor_equiv(&eta.target, &theta.source)
}
pub fn vertical_compose(eta: &Protolens, theta: &Protolens) -> Result<Protolens, LensError> {
if !protolens_composable(eta, theta) {
return Err(LensError::CompositionMismatch);
}
let complement = ComplementConstructor::Composite(vec![
eta.complement_constructor.clone(),
theta.complement_constructor.clone(),
]);
Ok(Protolens {
name: Name::from(format!("{}.{}", theta.name, eta.name)),
source: eta.source.clone(),
target: theta.target.clone(),
complement_constructor: complement,
})
}
pub fn horizontal_compose(eta: &Protolens, theta: &Protolens) -> Result<Protolens, LensError> {
let source = eta.source.compose(&theta.source);
let target = eta.target.compose(&theta.target);
let complement = ComplementConstructor::Composite(vec![
eta.complement_constructor.clone(),
theta.complement_constructor.clone(),
]);
Ok(Protolens {
name: Name::from(format!("{}*{}", theta.name, eta.name)),
source,
target,
complement_constructor: complement,
})
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProtolensChain {
pub steps: Vec<Protolens>,
}
impl ProtolensChain {
#[must_use]
pub const fn new(steps: Vec<Protolens>) -> Self {
Self { steps }
}
#[must_use]
pub fn composed_optic_kind(&self) -> crate::optic::OpticKind {
self.steps.iter().map(Protolens::optic_kind).fold(
crate::optic::OpticKind::Iso,
crate::optic::OpticKind::compose,
)
}
#[must_use]
pub fn applicable_to_with(&self, schema: &Schema, protocol: &Protocol) -> bool {
self.check_applicability_with(schema, protocol).is_ok()
}
#[must_use]
pub fn applicable_to(&self, schema: &Schema) -> bool {
if self.steps.is_empty() {
return true;
}
self.steps[0].applicable_to(schema)
}
pub fn instantiate(&self, schema: &Schema, protocol: &Protocol) -> Result<Lens, LensError> {
if self.steps.is_empty() {
return Ok(identity_lens(schema));
}
if self.steps.len() == 1 {
return self.steps[0].instantiate(schema, protocol);
}
let fused = self.fuse()?;
fused.instantiate(schema, protocol)
}
pub fn instantiate_sequential(
&self,
schema: &Schema,
protocol: &Protocol,
) -> Result<Lens, LensError> {
if self.steps.is_empty() {
return Ok(identity_lens(schema));
}
if self.steps.len() == 1 {
return self.steps[0].instantiate(schema, protocol);
}
for window in self.steps.windows(2) {
if !protolens_composable(&window[0], &window[1]) {
return Err(LensError::CompositionMismatch);
}
}
let mut running_schema = schema.clone();
let mut steps_iter = self.steps.iter();
let first = steps_iter.next().ok_or_else(|| {
LensError::ProtolensError("chain unexpectedly empty after non-empty check".into())
})?;
if !first.applicable_to(&running_schema) {
return Err(LensError::ProtolensError(format!(
"step `{}` not applicable to running schema",
first.name,
)));
}
let mut composed: Lens = first.instantiate(&running_schema, protocol)?;
running_schema = composed.tgt_schema.clone();
for step in steps_iter {
if !step.applicable_to(&running_schema) {
return Err(LensError::ProtolensError(format!(
"step `{}` not applicable to running schema",
step.name,
)));
}
let step_lens = step.instantiate(&running_schema, protocol)?;
running_schema = step_lens.tgt_schema.clone();
composed = crate::compose::compose(&composed, &step_lens)?;
}
Ok(composed)
}
pub fn instantiate_edit(
&self,
schema: &Schema,
protocol: &Protocol,
) -> Result<crate::EditLens, LensError> {
let base_lens = self.instantiate(schema, protocol)?;
Ok(crate::EditLens::from_lens(base_lens, protocol.clone()))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.steps.len()
}
pub fn check_applicability(&self, schema: &Schema) -> Result<(), Vec<String>> {
if self.steps.is_empty() {
return Ok(());
}
self.steps[0].check_applicability(schema)
}
pub fn check_applicability_with(
&self,
schema: &Schema,
protocol: &Protocol,
) -> Result<(), Vec<String>> {
if self.steps.is_empty() {
return Ok(());
}
for window in self.steps.windows(2) {
if !protolens_composable(&window[0], &window[1]) {
return Err(vec![format!(
"adjacent steps disagree: `{}.target` ≢ `{}.source`",
window[0].name, window[1].name,
)]);
}
}
let mut running = schema.clone();
for step in &self.steps {
step.check_applicability(&running)?;
running = step
.target_schema(&running, protocol)
.map_err(|e| vec![format!("step `{}` transform failed: {e}", step.name)])?;
}
Ok(())
}
pub fn fuse(&self) -> Result<Protolens, LensError> {
if self.steps.is_empty() {
return Err(LensError::ProtolensError("cannot fuse empty chain".into()));
}
if self.steps.len() == 1 {
return Ok(self.steps[0].clone());
}
for window in self.steps.windows(2) {
if !protolens_composable(&window[0], &window[1]) {
return Err(LensError::CompositionMismatch);
}
}
let source = self.steps[0].source.clone();
let mut combined_transform = self.steps[0].target.transform.clone();
for step in &self.steps[1..] {
combined_transform = TheoryTransform::Compose(
Box::new(combined_transform),
Box::new(step.target.transform.clone()),
);
}
let target = TheoryEndofunctor {
name: Arc::from(
self.steps
.iter()
.map(|s| s.target.name.to_string())
.collect::<Vec<_>>()
.join("."),
),
precondition: source.precondition.clone(),
transform: combined_transform,
};
let sub_complements: Vec<_> = self
.steps
.iter()
.map(|s| s.complement_constructor.clone())
.collect();
let complement = if sub_complements
.iter()
.all(|c| matches!(c, ComplementConstructor::Empty))
{
ComplementConstructor::Empty
} else {
ComplementConstructor::Composite(sub_complements)
};
let name = Name::from(
self.steps
.iter()
.map(|s| s.name.to_string())
.collect::<Vec<_>>()
.join("."),
);
Ok(Protolens {
name,
source,
target,
complement_constructor: complement,
})
}
}
pub struct FleetResult {
pub applied: Vec<(Name, Lens)>,
pub skipped: Vec<(Name, Vec<String>)>,
}
#[must_use]
pub fn apply_to_fleet(
chain: &ProtolensChain,
schemas: &[(Name, Schema)],
protocol: &Protocol,
) -> FleetResult {
let mut applied = Vec::new();
let mut skipped = Vec::new();
for (name, schema) in schemas {
let check = if chain.steps.is_empty() {
Ok(())
} else {
chain.steps[0].check_applicability(schema)
};
match check {
Err(reasons) => {
skipped.push((name.clone(), reasons));
}
Ok(()) => match chain.instantiate(schema, protocol) {
Ok(lens) => applied.push((name.clone(), lens)),
Err(e) => skipped.push((name.clone(), vec![format!("instantiation failed: {e}")])),
},
}
}
FleetResult { applied, skipped }
}
fn lift_constraint(
constraint: &panproto_gat::TheoryConstraint,
morphism: &panproto_gat::TheoryMorphism,
) -> panproto_gat::TheoryConstraint {
use panproto_gat::TheoryConstraint as TC;
match constraint {
TC::Unconstrained => TC::Unconstrained,
TC::HasSort(s) => {
let lifted = morphism.sort_map.get(s).unwrap_or(s);
TC::HasSort(Arc::clone(lifted))
}
TC::HasOp(o) => {
let lifted = morphism.op_map.get(o).unwrap_or(o);
TC::HasOp(Arc::clone(lifted))
}
TC::HasEquation(e) => TC::HasEquation(Arc::clone(e)),
TC::All(cs) => TC::All(cs.iter().map(|c| lift_constraint(c, morphism)).collect()),
TC::Any(cs) => TC::Any(cs.iter().map(|c| lift_constraint(c, morphism)).collect()),
TC::Not(c) => TC::Not(Box::new(lift_constraint(c, morphism))),
TC::HasDirectedEq(_)
| TC::HasValSort(_)
| TC::HasCoercion { .. }
| TC::HasMerger(_)
| TC::HasPolicy(_) => constraint.clone(),
}
}
fn lift_endofunctor(
ef: &TheoryEndofunctor,
morphism: &panproto_gat::TheoryMorphism,
) -> TheoryEndofunctor {
let lifted_precondition = lift_constraint(&ef.precondition, morphism);
let pullback_transform = TheoryTransform::Pullback(morphism.clone());
let lifted_transform = if matches!(ef.transform, TheoryTransform::Identity) {
pullback_transform
} else {
TheoryTransform::Compose(Box::new(pullback_transform), Box::new(ef.transform.clone()))
};
TheoryEndofunctor {
name: Arc::from(format!("{}[{}]", ef.name, morphism.name)),
precondition: lifted_precondition,
transform: lifted_transform,
}
}
#[must_use]
pub fn lift_protolens(protolens: &Protolens, morphism: &panproto_gat::TheoryMorphism) -> Protolens {
Protolens {
name: Name::from(format!("{}[{}]", protolens.name, morphism.name)),
source: lift_endofunctor(&protolens.source, morphism),
target: lift_endofunctor(&protolens.target, morphism),
complement_constructor: protolens.complement_constructor.clone(),
}
}
#[must_use]
pub fn lift_chain(
chain: &ProtolensChain,
morphism: &panproto_gat::TheoryMorphism,
) -> ProtolensChain {
ProtolensChain::new(
chain
.steps
.iter()
.map(|s| lift_protolens(s, morphism))
.collect(),
)
}
pub mod elementary {
use panproto_gat::{
DirectedEquation, Equation, Name, Operation, Sort, TheoryConstraint, TheoryEndofunctor,
TheoryMorphism, TheoryTransform,
};
use panproto_inst::value::Value;
use std::sync::Arc;
use super::{ComplementConstructor, Protolens, name_arc_clone};
const fn value_kind_slug(kind: panproto_gat::ValueKind) -> &'static str {
match kind {
panproto_gat::ValueKind::Bool => "bool",
panproto_gat::ValueKind::Int => "int",
panproto_gat::ValueKind::Float => "float",
panproto_gat::ValueKind::Str => "str",
panproto_gat::ValueKind::Bytes => "bytes",
panproto_gat::ValueKind::Token => "token",
panproto_gat::ValueKind::Null => "null",
panproto_gat::ValueKind::Any => "any",
}
}
#[must_use]
pub fn add_sort(
sort_name: impl Into<Name>,
vertex_kind: impl Into<Name>,
default: Value,
) -> Protolens {
let sort_name = sort_name.into();
let vertex_kind = vertex_kind.into();
Protolens {
name: Name::from(format!("add_sort_{sort_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("add_{sort_name}")),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::AddSort {
sort: Sort::simple(name_arc_clone(&sort_name)),
vertex_kind: Some(Arc::from(&*vertex_kind)),
},
},
complement_constructor: ComplementConstructor::AddedElement {
element_name: sort_name,
element_kind: format!("{vertex_kind}"),
default_value: Some(default),
},
}
}
#[must_use]
pub fn add_sort_with_default(
sort_name: impl Into<Name>,
vertex_kind: impl Into<Name>,
default_expr: panproto_expr::Expr,
) -> Protolens {
let sort_name = sort_name.into();
let vertex_kind = vertex_kind.into();
Protolens {
name: Name::from(format!("add_sort_with_default_{sort_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("add_{sort_name}")),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::AddSortWithDefault {
sort: Sort::simple(name_arc_clone(&sort_name)),
vertex_kind: Some(Arc::from(&*vertex_kind)),
default_expr,
},
},
complement_constructor: ComplementConstructor::AddedElement {
element_name: sort_name,
element_kind: format!("{vertex_kind}"),
default_value: None,
},
}
}
#[must_use]
pub fn drop_sort(sort_name: impl Into<Name>) -> Protolens {
let sort_name = sort_name.into();
let arc = name_arc_clone(&sort_name);
Protolens {
name: Name::from(format!("drop_sort_{sort_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::HasSort(Arc::clone(&arc)),
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("drop_{sort_name}")),
precondition: TheoryConstraint::HasSort(Arc::clone(&arc)),
transform: TheoryTransform::DropSort(Arc::clone(&arc)),
},
complement_constructor: ComplementConstructor::DroppedSortData { sort: sort_name },
}
}
#[must_use]
pub fn rename_sort(old: impl Into<Name>, new: impl Into<Name>) -> Protolens {
let old = old.into();
let new = new.into();
let old_arc = name_arc_clone(&old);
let new_arc = name_arc_clone(&new);
Protolens {
name: Name::from(format!("rename_sort_{old}_{new}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::HasSort(Arc::clone(&old_arc)),
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("rename_{old}")),
precondition: TheoryConstraint::HasSort(Arc::clone(&old_arc)),
transform: TheoryTransform::RenameSort {
old: old_arc,
new: new_arc,
},
},
complement_constructor: ComplementConstructor::Empty,
}
}
#[must_use]
pub fn add_op(
op_name: impl Into<Name>,
src_sort: impl Into<Name>,
tgt_sort: impl Into<Name>,
kind: impl Into<Name>,
) -> Protolens {
let op_name = op_name.into();
let src_sort = src_sort.into();
let tgt_sort = tgt_sort.into();
let kind = kind.into();
Protolens {
name: Name::from(format!("add_op_{op_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("add_{op_name}")),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::AddOp(Operation::unary(
name_arc_clone(&op_name),
name_arc_clone(&kind),
name_arc_clone(&src_sort),
name_arc_clone(&tgt_sort),
)),
},
complement_constructor: ComplementConstructor::AddedElement {
element_name: op_name,
element_kind: format!("{kind}"),
default_value: None,
},
}
}
#[must_use]
pub fn drop_op(op_name: impl Into<Name>) -> Protolens {
let op_name = op_name.into();
let arc = name_arc_clone(&op_name);
Protolens {
name: Name::from(format!("drop_op_{op_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::HasOp(Arc::clone(&arc)),
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("drop_{op_name}")),
precondition: TheoryConstraint::HasOp(Arc::clone(&arc)),
transform: TheoryTransform::DropOp(Arc::clone(&arc)),
},
complement_constructor: ComplementConstructor::DroppedOpData { op: op_name },
}
}
#[must_use]
pub fn add_edge(
src_sort: impl Into<Name>,
tgt_sort: impl Into<Name>,
edge_name: impl Into<Name>,
edge_kind: impl Into<Name>,
) -> Protolens {
let src_sort = src_sort.into();
let tgt_sort = tgt_sort.into();
let edge_name = edge_name.into();
let edge_kind = edge_kind.into();
let src_arc = name_arc_clone(&src_sort);
let tgt_arc = name_arc_clone(&tgt_sort);
let name_arc = name_arc_clone(&edge_name);
let kind_arc = name_arc_clone(&edge_kind);
Protolens {
name: Name::from(format!("add_edge_{src_sort}_{tgt_sort}_{edge_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("add_edge_{edge_name}")),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::AddEdge {
src_sort: src_arc,
tgt_sort: tgt_arc,
edge_name: name_arc,
edge_kind: kind_arc,
},
},
complement_constructor: ComplementConstructor::AddedElement {
element_name: edge_name,
element_kind: format!("{edge_kind}"),
default_value: None,
},
}
}
#[must_use]
pub fn drop_edge(
src_sort: impl Into<Name>,
tgt_sort: impl Into<Name>,
edge_name: Option<Name>,
) -> Protolens {
let src_sort = src_sort.into();
let tgt_sort = tgt_sort.into();
let src_arc = name_arc_clone(&src_sort);
let tgt_arc = name_arc_clone(&tgt_sort);
let name_arc: Option<Arc<str>> = edge_name.as_ref().map(name_arc_clone);
let label_display = edge_name
.as_ref()
.map_or_else(|| "unnamed".to_string(), ToString::to_string);
Protolens {
name: Name::from(format!("drop_edge_{src_sort}_{tgt_sort}_{label_display}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("drop_edge_{label_display}")),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::DropEdge {
src_sort: src_arc,
tgt_sort: tgt_arc,
edge_name: name_arc,
},
},
complement_constructor: ComplementConstructor::DroppedEdge {
src: src_sort,
tgt: tgt_sort,
edge_name,
edge_kind: Name::from(""),
},
}
}
#[must_use]
pub fn rename_op(old: impl Into<Name>, new: impl Into<Name>) -> Protolens {
let old = old.into();
let new = new.into();
let old_arc = name_arc_clone(&old);
let new_arc = name_arc_clone(&new);
Protolens {
name: Name::from(format!("rename_op_{old}_{new}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::HasOp(Arc::clone(&old_arc)),
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("rename_{old}")),
precondition: TheoryConstraint::HasOp(Arc::clone(&old_arc)),
transform: TheoryTransform::RenameOp {
old: old_arc,
new: new_arc,
},
},
complement_constructor: ComplementConstructor::Empty,
}
}
#[must_use]
pub fn add_equation(eq: Equation) -> Protolens {
let eq_name = Arc::clone(&eq.name);
Protolens {
name: Name::from(format!("add_eq_{eq_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("add_{eq_name}")),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::AddEquation(eq),
},
complement_constructor: ComplementConstructor::Empty,
}
}
#[must_use]
pub fn drop_equation(eq_name: impl Into<Name>) -> Protolens {
let eq_name = eq_name.into();
let arc = name_arc_clone(&eq_name);
Protolens {
name: Name::from(format!("drop_eq_{eq_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::HasEquation(Arc::clone(&arc)),
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("drop_{eq_name}")),
precondition: TheoryConstraint::HasEquation(Arc::clone(&arc)),
transform: TheoryTransform::DropEquation(Arc::clone(&arc)),
},
complement_constructor: ComplementConstructor::Empty,
}
}
#[must_use]
pub fn pullback(morphism: TheoryMorphism) -> Protolens {
let morph_name = Arc::clone(&morphism.name);
Protolens {
name: Name::from(format!("pullback_{morph_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("pullback_{morph_name}")),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::Pullback(morphism),
},
complement_constructor: ComplementConstructor::Empty,
}
}
#[must_use]
pub fn directed_eq(deq: DirectedEquation) -> Protolens {
let deq_name = Arc::clone(&deq.name);
let has_inverse = deq.inverse.is_some();
Protolens {
name: Name::from(format!("directed_eq_{deq_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("add_deq_{deq_name}")),
precondition: TheoryConstraint::Unconstrained,
transform: TheoryTransform::AddDirectedEquation(deq),
},
complement_constructor: if has_inverse {
ComplementConstructor::Empty
} else {
ComplementConstructor::DroppedOpData {
op: Name::from(&*deq_name),
}
},
}
}
#[must_use]
pub fn drop_directed_eq(deq_name: impl Into<Name>) -> Protolens {
let deq_name = deq_name.into();
let arc = name_arc_clone(&deq_name);
Protolens {
name: Name::from(format!("drop_deq_{deq_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::HasDirectedEq(Arc::clone(&arc)),
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("drop_deq_{deq_name}")),
precondition: TheoryConstraint::HasDirectedEq(Arc::clone(&arc)),
transform: TheoryTransform::DropDirectedEquation(Arc::clone(&arc)),
},
complement_constructor: ComplementConstructor::Empty,
}
}
#[must_use]
pub fn rename_edge_name(
src_sort: impl Into<Name>,
tgt_sort: impl Into<Name>,
old_name: impl Into<Name>,
new_name: impl Into<Name>,
) -> Protolens {
let src_sort = src_sort.into();
let tgt_sort = tgt_sort.into();
let old_name = old_name.into();
let new_name = new_name.into();
let src_arc = name_arc_clone(&src_sort);
let tgt_arc = name_arc_clone(&tgt_sort);
let old_arc = name_arc_clone(&old_name);
let new_arc = name_arc_clone(&new_name);
Protolens {
name: Name::from(format!("rename_edge_{old_name}_{new_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::All(vec![
TheoryConstraint::HasSort(Arc::clone(&src_arc)),
TheoryConstraint::HasSort(Arc::clone(&tgt_arc)),
]),
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("rename_edge_{old_name}_{new_name}")),
precondition: TheoryConstraint::All(vec![
TheoryConstraint::HasSort(Arc::clone(&src_arc)),
TheoryConstraint::HasSort(Arc::clone(&tgt_arc)),
]),
transform: TheoryTransform::RenameEdgeName {
src_sort: src_arc,
tgt_sort: tgt_arc,
old_name: old_arc,
new_name: new_arc,
},
},
complement_constructor: ComplementConstructor::Empty,
}
}
#[must_use]
pub fn sort_coerce(
sort_name: impl Into<Name>,
target_kind: panproto_gat::ValueKind,
coercion_expr: panproto_expr::Expr,
inverse_expr: Option<panproto_expr::Expr>,
coercion_class: panproto_gat::CoercionClass,
) -> Protolens {
let sort_name = sort_name.into();
let arc = name_arc_clone(&sort_name);
let complement_constructor = if matches!(coercion_class, panproto_gat::CoercionClass::Iso) {
ComplementConstructor::Empty
} else {
ComplementConstructor::CoercedSortData {
sort: sort_name.clone(),
class: coercion_class,
}
};
let target_kind_label = value_kind_slug(target_kind);
Protolens {
name: Name::from(format!("sort_coerce_{sort_name}_to_{target_kind_label}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::HasSort(Arc::clone(&arc)),
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("coerce_{sort_name}_to_{target_kind_label}")),
precondition: TheoryConstraint::HasSort(Arc::clone(&arc)),
transform: TheoryTransform::CoerceSort {
sort_name: Arc::clone(&arc),
target_kind,
coercion_expr,
inverse_expr,
coercion_class,
},
},
complement_constructor,
}
}
#[must_use]
pub fn scoped(focus: impl Into<Name>, inner: Protolens) -> Protolens {
let focus = focus.into();
let focus_arc = name_arc_clone(&focus);
let inner_name = inner.name;
let inner_transform = inner.target.transform;
let inner_complement = inner.complement_constructor;
Protolens {
name: Name::from(format!("scoped_{focus}_{inner_name}")),
source: TheoryEndofunctor {
name: Arc::from("id"),
precondition: TheoryConstraint::HasSort(Arc::clone(&focus_arc)),
transform: TheoryTransform::Identity,
},
target: TheoryEndofunctor {
name: Arc::from(&*format!("scope_{focus}")),
precondition: TheoryConstraint::HasSort(Arc::clone(&focus_arc)),
transform: TheoryTransform::ScopedTransform {
focus: focus_arc,
inner: Box::new(inner_transform),
},
},
complement_constructor: ComplementConstructor::Scoped {
focus,
inner: Box::new(inner_complement),
},
}
}
}
pub mod combinators {
use panproto_gat::Name;
use panproto_inst::value::Value;
use super::ProtolensChain;
use super::elementary;
#[must_use]
pub fn rename_field(
parent: impl Into<Name>,
field: impl Into<Name>,
old_name: impl Into<Name>,
new_name: impl Into<Name>,
) -> ProtolensChain {
let parent = parent.into();
let field = field.into();
let old_name = old_name.into();
let new_name = new_name.into();
ProtolensChain::new(vec![elementary::rename_edge_name(
parent, field, old_name, new_name,
)])
}
#[must_use]
pub fn remove_field(field: impl Into<Name>) -> ProtolensChain {
let field = field.into();
ProtolensChain::new(vec![elementary::drop_sort(field)])
}
#[must_use]
pub fn add_field(
parent: impl Into<Name>,
field_name: impl Into<Name>,
field_kind: impl Into<Name>,
default: Value,
) -> ProtolensChain {
let parent = parent.into();
let field_name = field_name.into();
let field_kind = field_kind.into();
ProtolensChain::new(vec![
elementary::add_sort(field_name.clone(), field_kind, default),
elementary::add_op(field_name.clone(), parent, field_name.clone(), field_name),
])
}
#[must_use]
pub fn hoist_field(
parent: impl Into<Name>,
intermediate: impl Into<Name>,
child: impl Into<Name>,
) -> ProtolensChain {
let parent = parent.into();
let intermediate = intermediate.into();
let child = child.into();
ProtolensChain::new(vec![
elementary::add_op(child.clone(), parent, child.clone(), child),
elementary::drop_sort(intermediate),
])
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn nest_field(
parent: impl Into<Name>,
child: impl Into<Name>,
new_intermediate: impl Into<Name>,
intermediate_kind: impl Into<Name>,
edge_kind: impl Into<Name>,
old_edge_name: Option<Name>,
parent_to_intermediate: impl Into<Name>,
intermediate_to_child: impl Into<Name>,
) -> ProtolensChain {
let parent = parent.into();
let child = child.into();
let new_intermediate = new_intermediate.into();
let intermediate_kind = intermediate_kind.into();
let edge_kind = edge_kind.into();
let parent_to_intermediate = parent_to_intermediate.into();
let intermediate_to_child = intermediate_to_child.into();
ProtolensChain::new(vec![
elementary::add_sort(new_intermediate.clone(), intermediate_kind, Value::Null),
elementary::add_edge(
parent.clone(),
new_intermediate.clone(),
parent_to_intermediate,
edge_kind.clone(),
),
elementary::add_edge(
new_intermediate,
child.clone(),
intermediate_to_child,
edge_kind,
),
elementary::drop_edge(parent, child, old_edge_name),
])
}
#[must_use]
pub fn pipeline(chains: Vec<ProtolensChain>) -> ProtolensChain {
let steps = chains.into_iter().flat_map(|c| c.steps).collect();
ProtolensChain::new(steps)
}
#[must_use]
pub fn map_items(focus: impl Into<Name>, inner: super::Protolens) -> super::Protolens {
elementary::scoped(focus, inner)
}
}
fn compute_migration_between(src: &Schema, tgt: &Schema) -> CompiledMigration {
let mut surviving_verts: HashSet<Name> = src
.vertices
.keys()
.filter(|v| tgt.vertices.contains_key(&**v))
.cloned()
.collect();
let surviving_edges: HashSet<Edge> = src
.edges
.keys()
.filter(|e| tgt.edges.contains_key(*e))
.cloned()
.collect();
let mut vertex_remap = HashMap::new();
let unmapped_src: Vec<&Name> = src
.vertices
.keys()
.filter(|v| !tgt.vertices.contains_key(&**v))
.collect();
let unmapped_tgt: Vec<&Name> = tgt
.vertices
.keys()
.filter(|v| !src.vertices.contains_key(&**v))
.collect();
for src_id in &unmapped_src {
if let Some(src_v) = src.vertices.get(*src_id) {
for tgt_id in &unmapped_tgt {
if let Some(tgt_v) = tgt.vertices.get(*tgt_id) {
if src_v.kind == tgt_v.kind
&& !vertex_remap.values().any(|v: &Name| v == *tgt_id)
{
surviving_verts.insert((*tgt_id).clone());
vertex_remap.insert((*src_id).clone(), (*tgt_id).clone());
break;
}
}
}
}
}
let mut final_surviving = surviving_verts;
for src_id in vertex_remap.keys() {
final_surviving.insert(src_id.clone());
}
let mut resolver = HashMap::new();
for edge in tgt.edges.keys() {
let src_in =
final_surviving.contains(&edge.src) || vertex_remap.values().any(|v| *v == edge.src);
let tgt_in =
final_surviving.contains(&edge.tgt) || vertex_remap.values().any(|v| *v == edge.tgt);
if src_in && tgt_in {
resolver.insert((edge.src.clone(), edge.tgt.clone()), edge.clone());
}
}
let expansion_path = compute_expansion_paths(src, tgt);
CompiledMigration {
surviving_verts: final_surviving,
surviving_edges,
vertex_remap,
edge_remap: HashMap::new(),
resolver,
hyper_resolver: HashMap::new(),
field_transforms: HashMap::new(),
conditional_survival: HashMap::new(),
expansion_path,
}
}
fn compute_expansion_paths(src: &Schema, tgt: &Schema) -> HashMap<(Name, Name), Vec<Name>> {
let mut paths: HashMap<(Name, Name), Vec<Name>> = HashMap::new();
let new_in_tgt: HashSet<Name> = tgt
.vertices
.keys()
.filter(|v| !src.vertices.contains_key(*v))
.cloned()
.collect();
if new_in_tgt.is_empty() {
return paths;
}
let mut src_pairs: HashSet<(Name, Name)> = HashSet::new();
for edge in src.edges.keys() {
src_pairs.insert((edge.src.clone(), edge.tgt.clone()));
}
let mut tgt_pairs: HashSet<(Name, Name)> = HashSet::new();
for edge in tgt.edges.keys() {
tgt_pairs.insert((edge.src.clone(), edge.tgt.clone()));
}
for (src_v, tgt_v) in src_pairs {
if !tgt.vertices.contains_key(&src_v) || !tgt.vertices.contains_key(&tgt_v) {
continue;
}
if tgt_pairs.contains(&(src_v.clone(), tgt_v.clone())) {
continue;
}
if let Some(intermediates) = bfs_through_new(tgt, &src_v, &tgt_v, &new_in_tgt) {
paths.insert((src_v, tgt_v), intermediates);
}
}
paths
}
fn bfs_through_new(
tgt: &Schema,
start: &Name,
end: &Name,
new_verts: &HashSet<Name>,
) -> Option<Vec<Name>> {
use std::collections::VecDeque;
let mut prev: HashMap<Name, Name> = HashMap::new();
let mut visited: HashSet<Name> = HashSet::new();
let mut queue: VecDeque<Name> = VecDeque::new();
visited.insert(start.clone());
queue.push_back(start.clone());
while let Some(v) = queue.pop_front() {
if v == *end {
let mut interior: Vec<Name> = Vec::new();
let mut cursor = v;
while let Some(p) = prev.get(&cursor) {
if *p == *start {
break;
}
interior.push(p.clone());
cursor = p.clone();
}
interior.reverse();
return if interior.is_empty() {
None
} else {
Some(interior)
};
}
if let Some(out_edges) = tgt.outgoing.get(&v) {
for edge in out_edges {
let next = &edge.tgt;
if visited.contains(next) {
continue;
}
let interior_ok = *next == *end || new_verts.contains(next);
if !interior_ok {
continue;
}
visited.insert(next.clone());
prev.insert(next.clone(), v.clone());
queue.push_back(next.clone());
}
}
}
None
}
#[allow(clippy::only_used_in_recursion, clippy::too_many_lines)]
fn apply_theory_transform_to_schema(
transform: &TheoryTransform,
schema: &Schema,
protocol: &Protocol,
) -> Result<Schema, LensError> {
match transform {
TheoryTransform::Identity
| TheoryTransform::AddDirectedEquation(_)
| TheoryTransform::DropDirectedEquation(_)
| TheoryTransform::AddEquation(_)
| TheoryTransform::DropEquation(_) => Ok(schema.clone()),
TheoryTransform::CoerceSort {
sort_name,
coercion_expr,
inverse_expr,
coercion_class,
..
} => Ok(apply_coerce_sort_to_schema(
schema,
sort_name,
coercion_expr,
inverse_expr.as_ref(),
*coercion_class,
)),
TheoryTransform::MergeSorts {
sort_a,
sort_b,
merged_name,
merger_expr,
} => Ok(apply_merge_sorts_to_schema(
schema,
sort_a,
sort_b,
merged_name,
merger_expr,
)),
TheoryTransform::RenameSort { old, new } => {
Ok(apply_rename_sort_to_schema(schema, old, new))
}
TheoryTransform::RenameOp { old, new } => Ok(apply_rename_op_to_schema(schema, old, new)),
TheoryTransform::DropSort(name) => Ok(apply_drop_sort_from_schema(schema, name)),
TheoryTransform::AddSort { sort, vertex_kind } => {
Ok(apply_add_sort(schema, sort, vertex_kind.as_ref(), None))
}
TheoryTransform::AddSortWithDefault {
sort,
vertex_kind,
default_expr,
} => Ok(apply_add_sort(
schema,
sort,
vertex_kind.as_ref(),
Some(default_expr),
)),
TheoryTransform::DropOp(name) => Ok(apply_drop_op_from_schema(schema, name)),
TheoryTransform::AddOp(op) => Ok(apply_add_op(schema, op)),
TheoryTransform::Pullback(morphism) => {
let mut result = schema.clone();
for (old, new) in &morphism.sort_map {
if old != new {
result = apply_rename_sort_to_schema(&result, old, new);
}
}
for (old, new) in &morphism.op_map {
if old != new {
result = apply_rename_op_to_schema(&result, old, new);
}
}
Ok(result)
}
TheoryTransform::RenameEdgeName {
src_sort,
tgt_sort,
old_name,
new_name,
} => Ok(apply_rename_edge_name(
schema, src_sort, tgt_sort, old_name, new_name,
)),
TheoryTransform::AddEdge {
src_sort,
tgt_sort,
edge_name,
edge_kind,
} => Ok(apply_add_edge_to_schema(
schema, src_sort, tgt_sort, edge_name, edge_kind,
)),
TheoryTransform::DropEdge {
src_sort,
tgt_sort,
edge_name,
} => Ok(apply_drop_edge_from_schema(
schema,
src_sort,
tgt_sort,
edge_name.as_ref(),
)),
TheoryTransform::ScopedTransform { focus, inner } => {
apply_scoped_schema_transform(schema, focus, inner, protocol)
}
TheoryTransform::Compose(first, second) => {
let intermediate = apply_theory_transform_to_schema(first, schema, protocol)?;
apply_theory_transform_to_schema(second, &intermediate, protocol)
}
TheoryTransform::StripEnrichment(kind) => Ok(apply_strip_enrichment(schema, *kind)),
TheoryTransform::AddEnrichment {
kind,
enricher,
policy,
} => {
let driver = crate::enrichment_registry::lookup_enricher(*kind, enricher)?;
driver.enrich(schema, policy)
}
}
}
fn apply_strip_enrichment(schema: &Schema, kind: panproto_gat::EnrichmentKind) -> Schema {
let mut out = schema.clone();
for cs in out.constraints.values_mut() {
cs.retain(|c| !kind.is_member_sort(c.sort.as_ref()));
}
out.constraints.retain(|_, cs| !cs.is_empty());
out
}
#[allow(clippy::only_used_in_recursion)]
fn apply_scoped_schema_transform(
schema: &Schema,
focus: &Arc<str>,
inner: &TheoryTransform,
protocol: &Protocol,
) -> Result<Schema, LensError> {
let mut reachable: std::collections::HashSet<Name> = std::collections::HashSet::new();
let mut queue: std::collections::VecDeque<Name> = std::collections::VecDeque::new();
let focus_name = Name::from(&**focus);
if schema.vertices.contains_key(&focus_name) {
reachable.insert(focus_name.clone());
queue.push_back(focus_name);
}
while let Some(v) = queue.pop_front() {
for edge in schema.outgoing_edges(&v) {
if reachable.insert(edge.tgt.clone()) {
queue.push_back(edge.tgt.clone());
}
}
}
let sub_vertices: HashMap<Name, Vertex> = schema
.vertices
.iter()
.filter(|(id, _)| reachable.contains(*id))
.map(|(id, v)| (id.clone(), v.clone()))
.collect();
let sub_edges: HashMap<panproto_schema::Edge, Name> = schema
.edges
.iter()
.filter(|(e, _)| reachable.contains(&e.src) && reachable.contains(&e.tgt))
.map(|(e, k)| (e.clone(), k.clone()))
.collect();
let sub_constraints: HashMap<Name, Vec<panproto_schema::Constraint>> = schema
.constraints
.iter()
.filter(|(id, _)| reachable.contains(*id))
.map(|(id, c)| (id.clone(), c.clone()))
.collect();
let sub_defaults: HashMap<Name, panproto_expr::Expr> = schema
.defaults
.iter()
.filter(|(id, _)| reachable.contains(*id))
.map(|(id, d)| (id.clone(), d.clone()))
.collect();
let mut sub_schema = schema.clone();
sub_schema.vertices = sub_vertices;
sub_schema.edges = sub_edges;
sub_schema.constraints = sub_constraints;
sub_schema.defaults = sub_defaults;
let transformed_sub = apply_theory_transform_to_schema(inner, &sub_schema, protocol)?;
let mut result = schema.clone();
result.vertices.retain(|id, _| !reachable.contains(id));
result
.edges
.retain(|e, _| !(reachable.contains(&e.src) && reachable.contains(&e.tgt)));
result.constraints.retain(|id, _| !reachable.contains(id));
result.defaults.retain(|id, _| !reachable.contains(id));
result.vertices.extend(transformed_sub.vertices);
result.edges.extend(transformed_sub.edges);
result.constraints.extend(transformed_sub.constraints);
result.defaults.extend(transformed_sub.defaults);
rebuild_adjacency(&mut result);
Ok(result)
}
fn apply_coerce_sort_to_schema(
schema: &Schema,
sort_name: &Arc<str>,
coercion_expr: &panproto_expr::Expr,
inverse_expr: Option<&panproto_expr::Expr>,
coercion_class: panproto_gat::CoercionClass,
) -> Schema {
let mut new_schema = schema.clone();
let name = Name::from(&**sort_name);
new_schema.coercions.insert(
(name.clone(), name),
panproto_schema::CoercionSpec {
forward: coercion_expr.clone(),
inverse: inverse_expr.cloned(),
class: coercion_class,
},
);
new_schema
}
fn apply_add_sort(
schema: &Schema,
sort: &panproto_gat::Sort,
vertex_kind: Option<&Arc<str>>,
default_expr: Option<&panproto_expr::Expr>,
) -> Schema {
let mut new_schema = schema.clone();
let name = Name::from(&*sort.name);
let kind = vertex_kind.map_or_else(|| sort.default_vertex_kind(), Arc::clone);
let vertex = Vertex {
id: name.clone(),
kind: Name::from(&*kind),
nsid: None,
};
new_schema.vertices.insert(name.clone(), vertex);
if let Some(expr) = default_expr {
new_schema.defaults.insert(name, expr.clone());
}
new_schema
}
fn apply_rename_edge_name(
schema: &Schema,
src_sort: &Arc<str>,
tgt_sort: &Arc<str>,
old_name: &Arc<str>,
new_name: &Arc<str>,
) -> Schema {
let mut new_edges = HashMap::new();
for (edge, kind) in &schema.edges {
let mut e = edge.clone();
if *e.src == **src_sort && *e.tgt == **tgt_sort && e.name.as_deref() == Some(&**old_name) {
e.name = Some(Name::from(&**new_name));
}
new_edges.insert(e, kind.clone());
}
let mut new_schema = schema.clone();
new_schema.edges = new_edges;
rebuild_adjacency(&mut new_schema);
new_schema
}
fn apply_add_op(schema: &Schema, op: &panproto_gat::Operation) -> Schema {
let mut new_schema = schema.clone();
let Some((_, src_sort, _)) = op.inputs.first() else {
return new_schema;
};
let src = Name::from(src_sort.head().as_ref());
let tgt = Name::from(op.output.head().as_ref());
if !new_schema.vertices.contains_key(&src) || !new_schema.vertices.contains_key(&tgt) {
return new_schema;
}
let edge = Edge {
src: src.clone(),
tgt: tgt.clone(),
kind: Name::from(&*op.name),
name: Some(Name::from(&*op.name)),
};
new_schema.edges.insert(edge.clone(), Name::from(&*op.name));
new_schema
.outgoing
.entry(src)
.or_default()
.push(edge.clone());
new_schema.incoming.entry(tgt).or_default().push(edge);
new_schema
}
fn apply_merge_sorts_to_schema(
schema: &Schema,
sort_a: &Arc<str>,
sort_b: &Arc<str>,
merged_name: &Arc<str>,
merger_expr: &panproto_expr::Expr,
) -> Schema {
let mut new_schema = apply_drop_sort_from_schema(schema, sort_a);
new_schema = apply_drop_sort_from_schema(&new_schema, sort_b);
let vertex = Vertex {
id: Name::from(&**merged_name),
kind: Name::from(&**merged_name),
nsid: None,
};
new_schema
.vertices
.insert(Name::from(&**merged_name), vertex);
new_schema
.mergers
.insert(Name::from(&**merged_name), merger_expr.clone());
new_schema
}
fn rebuild_adjacency(schema: &mut Schema) {
let mut outgoing: HashMap<Name, SmallVec<panproto_schema::Edge, 4>> = HashMap::new();
let mut incoming: HashMap<Name, SmallVec<panproto_schema::Edge, 4>> = HashMap::new();
let mut between: HashMap<(Name, Name), SmallVec<panproto_schema::Edge, 2>> = HashMap::new();
for edge in schema.edges.keys() {
outgoing
.entry(edge.src.clone())
.or_default()
.push(edge.clone());
incoming
.entry(edge.tgt.clone())
.or_default()
.push(edge.clone());
between
.entry((edge.src.clone(), edge.tgt.clone()))
.or_default()
.push(edge.clone());
}
schema.outgoing = outgoing;
schema.incoming = incoming;
schema.between = between;
}
fn apply_rename_sort_to_schema(schema: &Schema, old: &Arc<str>, new: &Arc<str>) -> Schema {
let mut new_schema = schema.clone();
let mut new_vertices = HashMap::new();
for (id, vertex) in &new_schema.vertices {
let mut v = vertex.clone();
if *v.kind == **old {
v.kind = Name::from(&**new);
}
new_vertices.insert(id.clone(), v);
}
new_schema.vertices = new_vertices;
let mut new_edges = HashMap::new();
for (edge, kind) in &new_schema.edges {
let e = edge.clone();
let k = if **kind == **old {
Name::from(&**new)
} else {
kind.clone()
};
new_edges.insert(e, k);
}
new_schema.edges = new_edges;
let mut new_coercions = HashMap::new();
for ((from, to), spec) in &new_schema.coercions {
let new_from = if **from == **old {
Name::from(&**new)
} else {
from.clone()
};
let new_to = if **to == **old {
Name::from(&**new)
} else {
to.clone()
};
new_coercions.insert((new_from, new_to), spec.clone());
}
new_schema.coercions = new_coercions;
for he in new_schema.hyper_edges.values_mut() {
he.signature = he
.signature
.iter()
.map(|(label, vid)| {
let new_vid = if **vid == **old {
Name::from(&**new)
} else {
vid.clone()
};
(label.clone(), new_vid)
})
.collect();
}
let mut new_constraints = HashMap::new();
for (cid, cs) in &new_schema.constraints {
let new_cs: Vec<_> = cs
.iter()
.map(|c| {
let mut c2 = c.clone();
if *c2.sort == **old {
c2.sort = Name::from(&**new);
}
c2
})
.collect();
new_constraints.insert(cid.clone(), new_cs);
}
new_schema.constraints = new_constraints;
rebuild_indices(&mut new_schema);
new_schema
}
fn apply_rename_op_to_schema(schema: &Schema, old: &Arc<str>, new: &Arc<str>) -> Schema {
let mut new_schema = schema.clone();
let mut new_edges = HashMap::new();
for (edge, kind) in &new_schema.edges {
let mut e = edge.clone();
if *e.kind == **old {
e.kind = Name::from(&**new);
}
let k = if **kind == **old {
Name::from(&**new)
} else {
kind.clone()
};
new_edges.insert(e, k);
}
new_schema.edges = new_edges;
rebuild_indices(&mut new_schema);
new_schema
}
fn apply_drop_sort_from_schema(schema: &Schema, name: &Arc<str>) -> Schema {
let mut new_schema = schema.clone();
let to_remove: Vec<Name> = new_schema
.vertices
.iter()
.filter(|(id, v)| **id == **name || *v.kind == **name)
.map(|(id, _)| id.clone())
.collect();
for id in &to_remove {
new_schema.vertices.remove(id);
}
let new_edges: HashMap<Edge, Name> = new_schema
.edges
.iter()
.filter(|(e, _)| !to_remove.contains(&e.src) && !to_remove.contains(&e.tgt))
.map(|(e, k)| (e.clone(), k.clone()))
.collect();
new_schema.edges = new_edges;
new_schema
.coercions
.retain(|(from, to), _| *from != **name && *to != **name);
new_schema
.mergers
.retain(|k, _| !to_remove.contains(k) && **k != **name);
new_schema
.defaults
.retain(|k, _| !to_remove.contains(k) && **k != **name);
new_schema
.policies
.retain(|k, _| !to_remove.contains(k) && **k != **name);
for id in &to_remove {
new_schema.constraints.remove(id);
}
rebuild_indices(&mut new_schema);
new_schema
}
fn apply_drop_op_from_schema(schema: &Schema, name: &Arc<str>) -> Schema {
let mut new_schema = schema.clone();
let new_edges: HashMap<Edge, Name> = new_schema
.edges
.iter()
.filter(|(e, _)| *e.kind != **name)
.map(|(e, k)| (e.clone(), k.clone()))
.collect();
new_schema.edges = new_edges;
rebuild_indices(&mut new_schema);
new_schema
}
fn apply_add_edge_to_schema(
schema: &Schema,
src_sort: &Arc<str>,
tgt_sort: &Arc<str>,
edge_name: &Arc<str>,
edge_kind: &Arc<str>,
) -> Schema {
let mut new_schema = schema.clone();
let src = Name::from(&**src_sort);
let tgt = Name::from(&**tgt_sort);
if !new_schema.vertices.contains_key(&src) || !new_schema.vertices.contains_key(&tgt) {
return new_schema;
}
let edge = Edge {
src: src.clone(),
tgt: tgt.clone(),
kind: Name::from(&**edge_kind),
name: Some(Name::from(&**edge_name)),
};
new_schema
.edges
.insert(edge.clone(), Name::from(&**edge_kind));
new_schema
.outgoing
.entry(src.clone())
.or_default()
.push(edge.clone());
new_schema
.incoming
.entry(tgt.clone())
.or_default()
.push(edge.clone());
new_schema.between.entry((src, tgt)).or_default().push(edge);
new_schema
}
fn apply_drop_edge_from_schema(
schema: &Schema,
src_sort: &Arc<str>,
tgt_sort: &Arc<str>,
edge_name: Option<&Arc<str>>,
) -> Schema {
let mut new_schema = schema.clone();
let target_name: Option<&str> = edge_name.map(|a| &**a);
let new_edges: HashMap<Edge, Name> = new_schema
.edges
.iter()
.filter(|(e, _)| {
let matches =
*e.src == **src_sort && *e.tgt == **tgt_sort && e.name.as_deref() == target_name;
!matches
})
.map(|(e, k)| (e.clone(), k.clone()))
.collect();
new_schema.edges = new_edges;
rebuild_indices(&mut new_schema);
new_schema
}
pub(crate) fn schema_to_implicit_theory(schema: &Schema) -> Theory {
let mut vertex_ids: Vec<&Name> = schema.vertices.keys().collect();
vertex_ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
let mut sort_names: HashSet<&str> = HashSet::new();
let mut sorts = Vec::new();
for vid in vertex_ids {
let vertex = &schema.vertices[vid];
if sort_names.insert(&vertex.kind) {
sorts.push(Sort::simple(name_arc_clone(&vertex.kind)));
}
}
let mut edges: Vec<&Edge> = schema.edges.keys().collect();
edges.sort_by(|a, b| {
a.src
.as_str()
.cmp(b.src.as_str())
.then_with(|| a.tgt.as_str().cmp(b.tgt.as_str()))
.then_with(|| a.kind.as_str().cmp(b.kind.as_str()))
});
let mut op_names: HashSet<&str> = HashSet::new();
let mut ops = Vec::new();
for edge in edges {
if op_names.insert(&edge.kind) {
let src_kind = schema
.vertices
.get(&edge.src)
.map_or_else(|| Arc::from("unknown"), |v| name_arc_clone(&v.kind));
let tgt_kind = schema
.vertices
.get(&edge.tgt)
.map_or_else(|| Arc::from("unknown"), |v| name_arc_clone(&v.kind));
ops.push(Operation::unary(
name_arc_clone(&edge.kind),
"x",
src_kind,
tgt_kind,
));
}
}
Theory::new("implicit", sorts, ops, Vec::new())
}
pub(crate) fn rebuild_indices(schema: &mut Schema) {
let mut outgoing: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
let mut incoming: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
let mut between: HashMap<(Name, Name), SmallVec<Edge, 2>> = HashMap::new();
let mut edges: Vec<&Edge> = schema.edges.keys().collect();
edges.sort_by(|a, b| {
a.src
.as_str()
.cmp(b.src.as_str())
.then_with(|| a.tgt.as_str().cmp(b.tgt.as_str()))
.then_with(|| a.kind.as_str().cmp(b.kind.as_str()))
.then_with(|| a.name.as_deref().cmp(&b.name.as_deref()))
});
for edge in edges {
outgoing
.entry(edge.src.clone())
.or_default()
.push(edge.clone());
incoming
.entry(edge.tgt.clone())
.or_default()
.push(edge.clone());
between
.entry((edge.src.clone(), edge.tgt.clone()))
.or_default()
.push(edge.clone());
}
schema.outgoing = outgoing;
schema.incoming = incoming;
schema.between = between;
}
fn identity_lens(schema: &Schema) -> Lens {
let surviving_verts = schema.vertices.keys().cloned().collect();
let surviving_edges = schema.edges.keys().cloned().collect();
Lens {
compiled: CompiledMigration {
surviving_verts,
surviving_edges,
vertex_remap: HashMap::new(),
edge_remap: HashMap::new(),
resolver: HashMap::new(),
hyper_resolver: HashMap::new(),
field_transforms: HashMap::new(),
conditional_survival: HashMap::new(),
expansion_path: HashMap::new(),
},
src_schema: schema.clone(),
tgt_schema: schema.clone(),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use panproto_inst::value::Value;
use panproto_schema::Protocol;
use super::{
ComplementConstructor, ProtolensChain, elementary, horizontal_compose, identity_lens,
schema_to_implicit_theory, vertical_compose,
};
use crate::tests::three_node_schema;
fn test_protocol() -> Protocol {
Protocol {
name: "test".into(),
schema_theory: "ThGraph".into(),
instance_theory: "ThWType".into(),
edge_rules: vec![],
obj_kinds: vec!["object".into(), "string".into(), "array".into()],
constraint_sorts: vec![],
..Protocol::default()
}
}
#[test]
fn schema_to_implicit_theory_deterministic_sort_op_order() {
use panproto_schema::SchemaBuilder;
let protocol = Protocol {
name: "t".into(),
schema_theory: "ThGraph".into(),
instance_theory: "ThWType".into(),
edge_rules: vec![],
obj_kinds: vec!["record".into(), "string".into(), "integer".into()],
constraint_sorts: vec![],
..Protocol::default()
};
let s = SchemaBuilder::new(&protocol)
.vertex("zzz", "record", None::<&str>)
.unwrap()
.vertex("aaa", "string", None::<&str>)
.unwrap()
.vertex("mmm", "integer", None::<&str>)
.unwrap()
.vertex("bbb", "string", None::<&str>)
.unwrap()
.edge("zzz", "aaa", "prop", Some("x"))
.unwrap()
.edge("zzz", "mmm", "field", Some("y"))
.unwrap()
.edge("zzz", "bbb", "attr", Some("z"))
.unwrap()
.build()
.unwrap();
let baseline = schema_to_implicit_theory(&s);
for _ in 0..16 {
let t = schema_to_implicit_theory(&s);
let baseline_sorts: Vec<String> =
baseline.sorts.iter().map(|x| x.name.to_string()).collect();
let t_sorts: Vec<String> = t.sorts.iter().map(|x| x.name.to_string()).collect();
assert_eq!(baseline_sorts, t_sorts, "sort order drift");
let baseline_ops: Vec<String> =
baseline.ops.iter().map(|x| x.name.to_string()).collect();
let t_ops: Vec<String> = t.ops.iter().map(|x| x.name.to_string()).collect();
assert_eq!(baseline_ops, t_ops, "op order drift");
}
}
#[test]
fn elementary_rename_sort_applicable() {
let schema = three_node_schema();
let p = elementary::rename_sort("string", "text");
assert!(p.applicable_to(&schema));
}
#[test]
fn elementary_rename_sort_not_applicable() {
let schema = three_node_schema();
let p = elementary::rename_sort("nonexistent", "text");
assert!(!p.applicable_to(&schema));
}
#[test]
fn elementary_drop_sort_applicable() {
let schema = three_node_schema();
let p = elementary::drop_sort("string");
assert!(p.applicable_to(&schema));
}
#[test]
fn elementary_add_sort_always_applicable() {
let schema = three_node_schema();
let p = elementary::add_sort("tags", "array", Value::Null);
assert!(p.applicable_to(&schema));
}
#[test]
fn elementary_rename_sort_instantiate() {
let schema = three_node_schema();
let protocol = test_protocol();
let p = elementary::rename_sort("string", "text");
let lens = p.instantiate(&schema, &protocol).unwrap();
assert_ne!(lens.src_schema.vertices.len(), 0);
}
#[test]
fn chain_empty_is_identity() {
let schema = three_node_schema();
let protocol = test_protocol();
let chain = ProtolensChain::new(vec![]);
let lens = chain.instantiate(&schema, &protocol).unwrap();
assert_eq!(
lens.src_schema.vertices.len(),
lens.tgt_schema.vertices.len()
);
}
#[test]
fn chain_single_step() {
let schema = three_node_schema();
let protocol = test_protocol();
let chain = ProtolensChain::new(vec![elementary::add_sort("tags", "array", Value::Null)]);
let lens = chain.instantiate(&schema, &protocol).unwrap();
assert_eq!(
lens.tgt_schema.vertices.len(),
lens.src_schema.vertices.len() + 1
);
}
#[test]
fn vertical_compose_works() {
let p1 = elementary::rename_sort("string", "text");
let p2 = elementary::add_sort("tags", "array", Value::Null);
let composed = vertical_compose(&p1, &p2).unwrap();
assert_eq!(&*composed.name, "add_sort_tags.rename_sort_string_text");
}
#[test]
fn is_lossless() {
assert!(elementary::rename_sort("a", "b").is_lossless());
assert!(elementary::rename_op("a", "b").is_lossless());
assert!(!elementary::add_sort("a", "b", Value::Null).is_lossless());
assert!(!elementary::drop_sort("a").is_lossless());
assert!(!elementary::drop_op("a").is_lossless());
}
#[test]
fn complement_constructor_types() {
assert!(matches!(
elementary::rename_sort("a", "b").complement_constructor,
ComplementConstructor::Empty
));
assert!(matches!(
elementary::drop_sort("a").complement_constructor,
ComplementConstructor::DroppedSortData { .. }
));
assert!(matches!(
elementary::drop_op("a").complement_constructor,
ComplementConstructor::DroppedOpData { .. }
));
assert!(matches!(
elementary::add_sort("a", "b", Value::Null).complement_constructor,
ComplementConstructor::AddedElement { .. }
));
}
#[test]
fn protolens_chain_applicable() {
let schema = three_node_schema();
let chain = ProtolensChain::new(vec![elementary::rename_sort("string", "text")]);
assert!(chain.applicable_to(&schema));
}
#[test]
fn schema_to_theory_extracts_kinds() {
let schema = three_node_schema();
let theory = schema_to_implicit_theory(&schema);
assert!(theory.has_sort("object"));
assert!(theory.has_sort("string"));
assert!(theory.has_op("prop"));
}
#[test]
fn horizontal_compose_works() {
let p1 = elementary::rename_sort("a", "b");
let p2 = elementary::rename_sort("c", "d");
let composed = horizontal_compose(&p1, &p2).unwrap();
assert!(composed.name.contains('*'));
}
#[test]
fn chain_len_and_is_empty() {
let empty = ProtolensChain::new(vec![]);
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
let chain = ProtolensChain::new(vec![elementary::rename_sort("a", "b")]);
assert!(!chain.is_empty());
assert_eq!(chain.len(), 1);
}
#[test]
fn drop_sort_instantiate() {
let schema = three_node_schema();
let protocol = test_protocol();
let p = elementary::drop_sort("string");
let lens = p.instantiate(&schema, &protocol).unwrap();
assert_eq!(lens.src_schema.vertices.len(), 3);
assert_eq!(lens.tgt_schema.vertices.len(), 1);
}
#[test]
fn add_sort_instantiate() {
let schema = three_node_schema();
let protocol = test_protocol();
let p = elementary::add_sort("tags", "array", Value::Null);
let lens = p.instantiate(&schema, &protocol).unwrap();
assert_eq!(lens.src_schema.vertices.len(), 3);
assert_eq!(lens.tgt_schema.vertices.len(), 4);
assert!(lens.tgt_schema.vertices.contains_key("tags"));
}
#[test]
fn target_schema_without_full_lens() {
let schema = three_node_schema();
let protocol = test_protocol();
let p = elementary::add_sort("tags", "array", Value::Null);
let tgt = p.target_schema(&schema, &protocol).unwrap();
assert_eq!(tgt.vertices.len(), 4);
}
#[test]
fn identity_lens_preserves_schema() {
let schema = three_node_schema();
let lens = identity_lens(&schema);
assert_eq!(
lens.src_schema.vertices.len(),
lens.tgt_schema.vertices.len()
);
assert_eq!(lens.src_schema.edges.len(), lens.tgt_schema.edges.len());
}
#[test]
fn serde_round_trip_protolens() {
let p = elementary::rename_sort("old", "new");
let json = p.to_json().unwrap();
let p2 = super::Protolens::from_json(&json).unwrap();
assert_eq!(&*p.name, &*p2.name);
}
#[test]
fn serde_round_trip_chain() {
let chain = ProtolensChain::new(vec![
elementary::rename_sort("a", "b"),
elementary::add_sort("c", "d", Value::Null),
elementary::drop_sort("e"),
]);
let json = chain.to_json().unwrap();
let chain2 = ProtolensChain::from_json(&json).unwrap();
assert_eq!(chain2.len(), 3);
assert_eq!(&*chain2.steps[0].name, &*chain.steps[0].name);
assert_eq!(&*chain2.steps[1].name, &*chain.steps[1].name);
assert_eq!(&*chain2.steps[2].name, &*chain.steps[2].name);
}
#[test]
fn serde_round_trip_pullback() {
use std::collections::HashMap;
let morphism = panproto_gat::TheoryMorphism {
name: std::sync::Arc::from("test_morph"),
domain: std::sync::Arc::from("T1"),
codomain: std::sync::Arc::from("T2"),
sort_map: HashMap::new(),
op_map: HashMap::new(),
};
let chain = ProtolensChain::new(vec![elementary::pullback(morphism)]);
let json = chain.to_json().unwrap();
let chain2 = ProtolensChain::from_json(&json).unwrap();
assert_eq!(chain2.len(), 1);
assert!(chain2.steps[0].name.contains("pullback"));
}
#[test]
fn serde_round_trip_composite_complement() {
let chain = ProtolensChain::new(vec![elementary::drop_sort("a"), elementary::drop_op("b")]);
let json = chain.to_json().unwrap();
let chain2 = ProtolensChain::from_json(&json).unwrap();
assert_eq!(chain2.len(), 2);
assert!(matches!(
chain2.steps[0].complement_constructor,
ComplementConstructor::DroppedSortData { .. }
));
assert!(matches!(
chain2.steps[1].complement_constructor,
ComplementConstructor::DroppedOpData { .. }
));
}
#[test]
fn schema_constraint_has_vertex_kind() {
use super::SchemaConstraint;
let schema = three_node_schema();
assert!(SchemaConstraint::HasVertexKind("object".into()).satisfied_by(&schema));
assert!(SchemaConstraint::HasVertexKind("string".into()).satisfied_by(&schema));
assert!(!SchemaConstraint::HasVertexKind("missing".into()).satisfied_by(&schema));
}
#[test]
fn schema_constraint_has_vertex() {
use super::SchemaConstraint;
let schema = three_node_schema();
assert!(SchemaConstraint::HasVertex("post:body".into()).satisfied_by(&schema));
assert!(!SchemaConstraint::HasVertex("nonexistent".into()).satisfied_by(&schema));
}
#[test]
fn schema_constraint_has_edge_kind() {
use super::SchemaConstraint;
let schema = three_node_schema();
assert!(SchemaConstraint::HasEdgeKind("prop".into()).satisfied_by(&schema));
assert!(!SchemaConstraint::HasEdgeKind("missing".into()).satisfied_by(&schema));
}
#[test]
fn schema_constraint_all_conjunction() {
use super::SchemaConstraint;
let schema = three_node_schema();
let both = SchemaConstraint::All(vec![
SchemaConstraint::HasVertexKind("object".into()),
SchemaConstraint::HasVertexKind("string".into()),
]);
assert!(both.satisfied_by(&schema));
let one_bad = SchemaConstraint::All(vec![
SchemaConstraint::HasVertexKind("object".into()),
SchemaConstraint::HasVertexKind("missing".into()),
]);
assert!(!one_bad.satisfied_by(&schema));
}
#[test]
fn check_applicability_returns_reasons() {
let schema = three_node_schema();
let p = super::Protolens {
name: panproto_gat::Name::from("test"),
source: panproto_gat::TheoryEndofunctor {
name: std::sync::Arc::from("id"),
precondition: panproto_gat::TheoryConstraint::HasSort(std::sync::Arc::from(
"missing",
)),
transform: panproto_gat::TheoryTransform::Identity,
},
target: panproto_gat::TheoryEndofunctor {
name: std::sync::Arc::from("id"),
precondition: panproto_gat::TheoryConstraint::Unconstrained,
transform: panproto_gat::TheoryTransform::Identity,
},
complement_constructor: ComplementConstructor::Empty,
};
let result = p.check_applicability(&schema);
assert!(result.is_err());
let reasons = result.unwrap_err();
assert!(!reasons.is_empty());
assert!(reasons[0].contains("missing"));
}
#[test]
fn from_theory_constraint_maps_has_sort() {
use super::SchemaConstraint;
let tc = panproto_gat::TheoryConstraint::HasSort(std::sync::Arc::from("Vertex"));
let sc = SchemaConstraint::from_theory_constraint(&tc);
assert!(matches!(sc, SchemaConstraint::HasVertexKind(ref n) if &**n == "Vertex"));
}
fn make_schema_with_kind(
name: &str,
kind: &str,
) -> (panproto_gat::Name, panproto_schema::Schema) {
use panproto_schema::Vertex;
use std::collections::HashMap;
let mut vertices = HashMap::new();
vertices.insert(
panproto_gat::Name::from(format!("{name}:v1")),
Vertex {
id: format!("{name}:v1").into(),
kind: kind.into(),
nsid: None,
},
);
let schema = panproto_schema::Schema {
protocol: String::new(),
vertices,
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(),
};
(panproto_gat::Name::from(name), schema)
}
fn make_string_schema(name: &str) -> (panproto_gat::Name, panproto_schema::Schema) {
make_schema_with_kind(name, "string")
}
fn make_non_string_schema(name: &str) -> (panproto_gat::Name, panproto_schema::Schema) {
make_schema_with_kind(name, "integer")
}
#[test]
fn fleet_all_applicable() {
let protocol = test_protocol();
let chain = ProtolensChain::new(vec![elementary::rename_sort("string", "text")]);
let schemas: Vec<_> = vec![
make_string_schema("a"),
make_string_schema("b"),
make_string_schema("c"),
];
let result = super::apply_to_fleet(&chain, &schemas, &protocol);
assert_eq!(result.applied.len(), 3);
assert_eq!(result.skipped.len(), 0);
}
#[test]
fn fleet_partial_applicable() {
let protocol = test_protocol();
let chain = ProtolensChain::new(vec![elementary::rename_sort("string", "text")]);
let schemas: Vec<_> = vec![
make_string_schema("a"),
make_string_schema("b"),
make_non_string_schema("c"),
];
let result = super::apply_to_fleet(&chain, &schemas, &protocol);
assert_eq!(result.applied.len(), 2);
assert_eq!(result.skipped.len(), 1);
}
#[test]
fn fleet_empty_chain() {
let protocol = test_protocol();
let chain = ProtolensChain::new(vec![]);
let schemas: Vec<_> = vec![
make_string_schema("a"),
make_string_schema("b"),
make_string_schema("c"),
];
let result = super::apply_to_fleet(&chain, &schemas, &protocol);
assert_eq!(result.applied.len(), 3);
assert_eq!(result.skipped.len(), 0);
}
#[test]
fn check_applicability_chain_delegates() {
let schema = three_node_schema();
let chain = ProtolensChain::new(vec![elementary::rename_sort("string", "text")]);
assert!(chain.check_applicability(&schema).is_ok());
let bad_chain = ProtolensChain::new(vec![elementary::rename_sort("nonexistent", "text")]);
assert!(bad_chain.check_applicability(&schema).is_err());
let empty_chain = ProtolensChain::new(vec![]);
assert!(empty_chain.check_applicability(&schema).is_ok());
}
#[test]
fn fuse_single_step() {
let chain = ProtolensChain::new(vec![elementary::rename_sort("string", "text")]);
let fused = chain.fuse().unwrap_or_else(|e| panic!("fuse failed: {e}"));
assert_eq!(&*fused.name, "rename_sort_string_text");
}
#[test]
fn fuse_two_steps() {
let chain = ProtolensChain::new(vec![
elementary::rename_sort("string", "text"),
elementary::add_sort("tags", "array", Value::Null),
]);
let fused = chain.fuse().unwrap_or_else(|e| panic!("fuse failed: {e}"));
assert!(
fused.name.contains("rename_sort_string_text"),
"fused name should contain first step name, got: {}",
fused.name
);
assert!(
fused.name.contains("add_sort_tags"),
"fused name should contain second step name, got: {}",
fused.name
);
}
#[test]
fn fuse_empty_chain_errors() {
let chain = ProtolensChain::new(vec![]);
let result = chain.fuse();
assert!(result.is_err());
}
#[test]
fn fused_preserves_complement() {
let chain =
ProtolensChain::new(vec![elementary::drop_sort("a"), elementary::drop_sort("b")]);
let fused = chain.fuse().unwrap_or_else(|e| panic!("fuse failed: {e}"));
assert!(
matches!(fused.complement_constructor, ComplementConstructor::Composite(ref v) if v.len() == 2),
"expected Composite complement with 2 entries"
);
}
fn test_morphism_vertex_to_node() -> panproto_gat::TheoryMorphism {
use std::collections::HashMap;
let mut sort_map = HashMap::new();
sort_map.insert(std::sync::Arc::from("Vertex"), std::sync::Arc::from("Node"));
panproto_gat::TheoryMorphism {
name: std::sync::Arc::from("rename_vertex_node"),
domain: std::sync::Arc::from("T1"),
codomain: std::sync::Arc::from("T2"),
sort_map,
op_map: HashMap::new(),
}
}
fn identity_morphism() -> panproto_gat::TheoryMorphism {
use std::collections::HashMap;
panproto_gat::TheoryMorphism {
name: std::sync::Arc::from("id"),
domain: std::sync::Arc::from("T"),
codomain: std::sync::Arc::from("T"),
sort_map: HashMap::new(),
op_map: HashMap::new(),
}
}
#[test]
fn lift_protolens_renames_precondition() {
let p = elementary::drop_sort("Vertex");
let morphism = test_morphism_vertex_to_node();
let lifted = super::lift_protolens(&p, &morphism);
match &lifted.source.precondition {
panproto_gat::TheoryConstraint::HasSort(s) => {
assert_eq!(&**s, "Node", "lifted precondition should reference 'Node'");
}
other => panic!("expected HasSort, got: {other:?}"),
}
}
#[test]
fn lift_protolens_identity_morphism() {
let p = elementary::drop_sort("Vertex");
let morphism = identity_morphism();
let lifted = super::lift_protolens(&p, &morphism);
match &lifted.source.precondition {
panproto_gat::TheoryConstraint::HasSort(s) => {
assert_eq!(&**s, "Vertex", "identity lift should preserve precondition");
}
other => panic!("expected HasSort, got: {other:?}"),
}
}
#[test]
fn lift_chain_preserves_length() {
let chain = ProtolensChain::new(vec![
elementary::rename_sort("a", "b"),
elementary::drop_sort("c"),
elementary::add_sort("d", "e", Value::Null),
]);
let morphism = identity_morphism();
let lifted = super::lift_chain(&chain, &morphism);
assert_eq!(lifted.len(), 3);
}
#[test]
fn lift_preserves_complement() {
let p = elementary::drop_sort("Vertex");
let morphism = test_morphism_vertex_to_node();
let lifted = super::lift_protolens(&p, &morphism);
assert!(
matches!(
lifted.complement_constructor,
ComplementConstructor::DroppedSortData { .. }
),
"complement should be preserved as DroppedSortData"
);
}
#[test]
fn lift_protolens_name() {
let p = elementary::drop_sort("Vertex");
let morphism = test_morphism_vertex_to_node();
let lifted = super::lift_protolens(&p, &morphism);
assert!(
lifted.name.contains("rename_vertex_node"),
"lifted name should include morphism name, got: {}",
lifted.name
);
}
use panproto_gat::Name as GatName;
use panproto_schema::{Edge as SchemaEdge, Vertex};
use smallvec::{SmallVec, smallvec};
#[test]
fn elementary_drop_edge_targets_only_the_named_edge() {
let schema = three_node_schema();
let protocol = test_protocol();
let p = elementary::drop_edge("post:body", "post:body.text", Some(GatName::from("text")));
let lens = p.instantiate(&schema, &protocol).unwrap();
let edges: Vec<_> = lens
.tgt_schema
.edges
.keys()
.map(|e| (e.src.clone(), e.tgt.clone(), e.name.clone()))
.collect();
assert!(
edges.iter().any(|(_, t, n)| {
**t == *"post:body.createdAt" && n.as_deref() == Some("createdAt")
}),
"createdAt edge should survive drop_edge, got {edges:?}"
);
assert!(
!edges
.iter()
.any(|(_, t, n)| **t == *"post:body.text" && n.as_deref() == Some("text")),
"text edge should have been dropped, got {edges:?}"
);
}
#[test]
fn elementary_add_edge_separates_name_from_kind() {
let schema = three_node_schema();
let protocol = test_protocol();
let p = elementary::add_edge(
"post:body",
"post:body.text",
"displayText", "prop", );
let lens = p.instantiate(&schema, &protocol).unwrap();
let new_edge = lens
.tgt_schema
.edges
.keys()
.find(|e| {
*e.src == *"post:body"
&& *e.tgt == *"post:body.text"
&& e.name.as_deref() == Some("displayText")
})
.expect("new edge should exist");
assert_eq!(&*new_edge.kind, "prop", "kind should be preserved as prop");
assert_eq!(
new_edge.name.as_deref(),
Some("displayText"),
"name should be the caller-supplied label"
);
}
#[test]
fn nest_field_handles_qualified_vertex_ids() {
let schema = three_node_schema();
let protocol = test_protocol();
let chain = super::combinators::nest_field(
"post:body", "post:body.text", "post:body.profile", "object", "prop", Some(GatName::from("text")), "profile", "text", );
let lens = chain
.instantiate(&schema, &protocol)
.expect("nest_field should instantiate against qualified ids");
let edges: Vec<_> = lens.tgt_schema.edges.keys().cloned().collect();
assert!(
edges.iter().any(|e| {
*e.src == *"post:body"
&& *e.tgt == *"post:body.profile"
&& e.name.as_deref() == Some("profile")
&& &*e.kind == "prop"
}),
"target schema should contain post:body -(profile)-> post:body.profile, got {edges:?}"
);
assert!(
edges.iter().any(|e| {
*e.src == *"post:body.profile"
&& *e.tgt == *"post:body.text"
&& e.name.as_deref() == Some("text")
&& &*e.kind == "prop"
}),
"target schema should contain post:body.profile -(text)-> post:body.text, got {edges:?}"
);
assert!(
!edges.iter().any(|e| {
*e.src == *"post:body"
&& *e.tgt == *"post:body.text"
&& e.name.as_deref() == Some("text")
}),
"original post:body -(text)-> post:body.text edge should be removed, got {edges:?}"
);
assert!(
edges.iter().any(|e| {
*e.src == *"post:body"
&& *e.tgt == *"post:body.createdAt"
&& e.name.as_deref() == Some("createdAt")
}),
"sibling createdAt edge should survive nest_field, got {edges:?}"
);
let intermediate = lens
.tgt_schema
.vertices
.get(&GatName::from("post:body.profile"))
.expect("intermediate vertex should exist");
assert_eq!(&*intermediate.kind, "object");
}
#[test]
fn nest_field_preserves_sibling_prop_edges() {
let schema = three_node_schema();
let protocol = test_protocol();
let chain = super::combinators::nest_field(
"post:body",
"post:body.text",
"post:body.wrapper",
"object",
"prop",
Some(GatName::from("text")),
"wrapper",
"text",
);
let lens = chain.instantiate(&schema, &protocol).unwrap();
let prop_edge_count = lens
.tgt_schema
.edges
.keys()
.filter(|e| &*e.kind == "prop")
.count();
assert_eq!(
prop_edge_count, 3,
"expected 3 prop edges after nest_field (createdAt + 2 new), got {prop_edge_count}"
);
}
#[test]
fn nest_field_forward_eval_synthesizes_intermediate_node() {
use crate::asymmetric;
use crate::tests::three_node_instance;
let schema = three_node_schema();
let instance = three_node_instance();
let protocol = test_protocol();
let chain = super::combinators::nest_field(
"post:body",
"post:body.text",
"post:body.profile", "object",
"prop",
Some(GatName::from("text")),
"profile",
"text",
);
let lens = chain.instantiate(&schema, &protocol).unwrap();
assert!(
!lens.compiled.expansion_path.is_empty(),
"compiled migration should contain an expansion_path entry"
);
let key = (GatName::from("post:body"), GatName::from("post:body.text"));
let intermediates = lens
.compiled
.expansion_path
.get(&key)
.expect("expansion_path should cover the dropped direct arc");
assert_eq!(
intermediates,
&vec![GatName::from("post:body.profile")],
"expansion should route through the new intermediate"
);
let (view, complement) = asymmetric::get(&lens, &instance)
.expect("forward eval should succeed on a nest_field chain");
let has_intermediate = view
.nodes
.values()
.any(|n| &*n.anchor == "post:body.profile");
assert!(
has_intermediate,
"view should contain a synthesized node anchored at post:body.profile, got {:?}",
view.nodes
.values()
.map(|n| n.anchor.clone())
.collect::<Vec<_>>()
);
assert_eq!(
complement.synthesized_nodes.len(),
1,
"exactly one node should have been synthesized"
);
let synth_id = *complement.synthesized_nodes.iter().next().unwrap();
assert!(!instance.nodes.contains_key(&synth_id));
let text_node_id = view
.nodes
.iter()
.find(|(_, n)| &*n.anchor == "post:body.text")
.map(|(id, _)| *id)
.expect("surviving text node");
let arc_to_text = view
.arcs
.iter()
.find(|(_, c, _)| *c == text_node_id)
.expect("arc pointing at text_node");
assert_eq!(
arc_to_text.0, synth_id,
"text should be downstream of synth"
);
assert_eq!(arc_to_text.2.name.as_deref(), Some("text"));
let arc_to_synth = view
.arcs
.iter()
.find(|(_, c, _)| *c == synth_id)
.expect("arc pointing at synth node");
assert_eq!(arc_to_synth.2.name.as_deref(), Some("profile"));
let has_createdat = view
.arcs
.iter()
.any(|(_, _, e)| e.name.as_deref() == Some("createdAt"));
assert!(
has_createdat,
"createdAt sibling arc should survive nest forward eval"
);
}
#[test]
fn nest_field_get_put_round_trip_recovers_source() {
use crate::asymmetric;
use crate::tests::three_node_instance;
let schema = three_node_schema();
let instance = three_node_instance();
let protocol = test_protocol();
let chain = super::combinators::nest_field(
"post:body",
"post:body.text",
"post:body.profile",
"object",
"prop",
Some(GatName::from("text")),
"profile",
"text",
);
let lens = chain.instantiate(&schema, &protocol).unwrap();
let (view, complement) = asymmetric::get(&lens, &instance).unwrap();
let restored = asymmetric::put(&lens, &view, &complement).unwrap();
assert_eq!(
restored.nodes.len(),
instance.nodes.len(),
"restored instance should have the same number of nodes as source"
);
for id in instance.nodes.keys() {
assert!(
restored.nodes.contains_key(id),
"source node {id} should be restored"
);
}
assert!(
!restored
.nodes
.values()
.any(|n| &*n.anchor == "post:body.profile"),
"synthesized intermediate must be dropped by put"
);
let has_direct_text_arc = restored.arcs.iter().any(|(p, c, e)| {
instance
.nodes
.get(p)
.is_some_and(|n| &*n.anchor == "post:body")
&& instance
.nodes
.get(c)
.is_some_and(|n| &*n.anchor == "post:body.text")
&& e.name.as_deref() == Some("text")
});
assert!(
has_direct_text_arc,
"put should restore the original direct `text` arc"
);
}
fn nested_schema(
root: &str,
intermediate: &str,
leaf: &str,
leaf_kind: &str,
) -> panproto_schema::Schema {
use std::collections::HashMap;
let mut vertices: HashMap<GatName, Vertex> = HashMap::new();
let mut edges: HashMap<SchemaEdge, GatName> = HashMap::new();
let mut outgoing: HashMap<GatName, smallvec::SmallVec<SchemaEdge, 4>> = HashMap::new();
let mut incoming: HashMap<GatName, smallvec::SmallVec<SchemaEdge, 4>> = HashMap::new();
let mut between: HashMap<(GatName, GatName), smallvec::SmallVec<SchemaEdge, 2>> =
HashMap::new();
for (id, kind) in [
(root, "object"),
(intermediate, "object"),
(leaf, leaf_kind),
] {
vertices.insert(
GatName::from(id),
Vertex {
id: id.into(),
kind: kind.into(),
nsid: None,
},
);
}
let mut add_edge = |src: &str, tgt: &str, name: &str| {
let e = SchemaEdge {
src: GatName::from(src),
tgt: GatName::from(tgt),
kind: "prop".into(),
name: Some(GatName::from(name)),
};
outgoing
.entry(GatName::from(src))
.or_default()
.push(e.clone());
incoming
.entry(GatName::from(tgt))
.or_default()
.push(e.clone());
between
.entry((GatName::from(src), GatName::from(tgt)))
.or_default()
.push(e.clone());
edges.insert(e, GatName::from("prop"));
};
add_edge(root, intermediate, intermediate);
add_edge(intermediate, leaf, leaf);
panproto_schema::Schema {
protocol: format!("test-{root}"),
vertices,
edges,
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::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(),
entries: vec![GatName::from(root)],
outgoing,
incoming,
between,
}
}
#[test]
fn hoist_field_get_put_json_round_trip_restores_nested_record() {
use crate::asymmetric;
use panproto_inst::parse::{parse_json, to_json};
let source = nested_schema("user", "profile", "name", "string");
let protocol = test_protocol();
let chain = super::combinators::hoist_field("user", "profile", "name");
let lens = chain
.instantiate(&source, &protocol)
.expect("hoist chain instantiate");
let input: serde_json::Value = serde_json::json!({"profile": {"name": "Alice"}});
let instance = parse_json(&source, "user", &input).expect("parse input");
let (view, complement) = asymmetric::get(&lens, &instance).expect("get");
let restored = asymmetric::put(&lens, &view, &complement).expect("put");
let restored_json = to_json(&source, &restored);
assert_eq!(
restored_json, input,
"hoist get/put must restore the source JSON byte-for-byte, got {restored_json}"
);
assert!(
restored_json["profile"].is_object(),
"restored profile must be a JSON object (record), not a list: {restored_json}"
);
assert_eq!(restored_json["profile"]["name"], serde_json::json!("Alice"));
}
#[test]
fn hoist_field_put_then_json_does_not_listify_record() {
use crate::asymmetric;
use panproto_inst::parse::{parse_json, to_json};
let source = nested_schema("user", "profile", "name", "string");
let protocol = test_protocol();
let chain = super::combinators::hoist_field("user", "profile", "name");
let lens = chain
.instantiate(&source, &protocol)
.expect("hoist chain instantiate");
let input: serde_json::Value = serde_json::json!({"profile": {"name": "Alice"}});
let instance = parse_json(&source, "user", &input).expect("parse input");
let (view, complement) = asymmetric::get(&lens, &instance).expect("get");
let restored = asymmetric::put(&lens, &view, &complement).expect("put");
let restored_json = to_json(&source, &restored);
assert!(
restored_json["profile"].is_object(),
"dropped `profile` sort must reconstruct as a record, not a Value::List: {restored_json}"
);
assert_eq!(restored_json["profile"]["name"], serde_json::json!("Alice"));
}
#[test]
fn drop_edge_schema_apply_rebuilds_indices() {
use std::collections::HashMap;
let mut vertices = HashMap::new();
vertices.insert(
GatName::from("a"),
Vertex {
id: "a".into(),
kind: "object".into(),
nsid: None,
},
);
vertices.insert(
GatName::from("b"),
Vertex {
id: "b".into(),
kind: "string".into(),
nsid: None,
},
);
let edge = SchemaEdge {
src: "a".into(),
tgt: "b".into(),
kind: "prop".into(),
name: Some("label".into()),
};
let mut edges = HashMap::new();
edges.insert(edge.clone(), GatName::from("prop"));
let mut outgoing = HashMap::new();
outgoing.insert(GatName::from("a"), smallvec![edge.clone()]);
let mut incoming = HashMap::new();
incoming.insert(GatName::from("b"), smallvec![edge.clone()]);
let mut between = HashMap::new();
between.insert((GatName::from("a"), GatName::from("b")), smallvec![edge]);
let schema = panproto_schema::Schema {
protocol: "test".into(),
vertices,
edges,
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,
incoming,
between,
};
let protocol = test_protocol();
let p = elementary::drop_edge("a", "b", Some(GatName::from("label")));
let lens = p.instantiate(&schema, &protocol).unwrap();
assert_eq!(lens.tgt_schema.edges.len(), 0);
let out = lens.tgt_schema.outgoing.get(&GatName::from("a"));
assert!(
out.is_none_or(SmallVec::is_empty),
"outgoing should be empty"
);
let inc = lens.tgt_schema.incoming.get(&GatName::from("b"));
assert!(
inc.is_none_or(SmallVec::is_empty),
"incoming should be empty"
);
}
}