use panproto_gat::TheoryTransform;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum OpticKind {
Iso,
Lens,
Prism,
Affine,
Traversal,
}
impl PartialOrd for OpticKind {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
use std::cmp::Ordering::{Greater, Less};
if self == other {
return Some(std::cmp::Ordering::Equal);
}
match (self, other) {
(Self::Iso, _) | (Self::Lens | Self::Prism, Self::Affine) => Some(Less),
(_, Self::Iso) | (Self::Traversal, _) | (Self::Affine, Self::Lens | Self::Prism) => {
Some(Greater)
}
(_, Self::Traversal) => Some(Less),
_ => None,
}
}
}
impl OpticKind {
#[must_use]
pub const fn compose(self, other: Self) -> Self {
match (self, other) {
(Self::Iso, x) | (x, Self::Iso) => x,
(Self::Traversal, _) | (_, Self::Traversal) => Self::Traversal,
(Self::Lens, Self::Lens) => Self::Lens,
(Self::Prism, Self::Prism) => Self::Prism,
_ => Self::Affine,
}
}
}
#[must_use]
pub fn classify_transform(transform: &TheoryTransform) -> OpticKind {
match transform {
TheoryTransform::Identity
| TheoryTransform::RenameSort { .. }
| TheoryTransform::RenameOp { .. }
| TheoryTransform::RenameEdgeName { .. } => OpticKind::Iso,
TheoryTransform::DropSort(_)
| TheoryTransform::DropOp(_)
| TheoryTransform::DropEquation(_)
| TheoryTransform::AddSort { .. }
| TheoryTransform::AddOp(_)
| TheoryTransform::AddEdge { .. }
| TheoryTransform::DropEdge { .. }
| TheoryTransform::AddEquation(_)
| TheoryTransform::Pullback(_)
| TheoryTransform::CoerceSort { .. }
| TheoryTransform::MergeSorts { .. }
| TheoryTransform::AddSortWithDefault { .. }
| TheoryTransform::AddDirectedEquation(_)
| TheoryTransform::DropDirectedEquation(_)
| TheoryTransform::StripEnrichment(_)
| TheoryTransform::AddEnrichment { .. } => OpticKind::Lens,
TheoryTransform::ScopedTransform { inner, .. } => {
let inner_kind = classify_transform(inner);
inner_kind.compose(OpticKind::Lens)
}
TheoryTransform::Compose(a, b) => {
let kind_a = classify_transform(a);
let kind_b = classify_transform(b);
kind_a.compose(kind_b)
}
}
}
#[must_use]
pub fn refine_scoped_optic(edge_kind: &str, inner_kind: OpticKind) -> OpticKind {
let carrier = match edge_kind {
"item" | "items" => OpticKind::Traversal,
"variant" => OpticKind::Prism,
_ => OpticKind::Lens,
};
carrier.compose(inner_kind)
}
pub fn check_optic_laws(
kind: OpticKind,
lens: &crate::Lens,
instance: &panproto_inst::WInstance,
) -> Result<(), OpticLawViolation> {
use crate::asymmetric::{get, put};
let (view, complement) = get(lens, instance).map_err(|e| OpticLawViolation {
kind,
law: "get",
detail: format!("get failed: {e}"),
})?;
let restored = put(lens, &view, &complement).map_err(|e| OpticLawViolation {
kind,
law: "PutGet",
detail: format!("put failed: {e}"),
})?;
if !crate::laws::instances_equivalent(instance, &restored) {
return Err(OpticLawViolation {
kind,
law: "PutGet",
detail: format!(
"structural mismatch: original {} nodes/{} arcs, restored {} nodes/{} arcs",
instance.node_count(),
instance.arc_count(),
restored.node_count(),
restored.arc_count()
),
});
}
let (view2, _complement2) = get(lens, &restored).map_err(|e| OpticLawViolation {
kind,
law: "GetPut",
detail: format!("get after put failed: {e}"),
})?;
if !crate::laws::instances_equivalent(&view, &view2) {
return Err(OpticLawViolation {
kind,
law: "GetPut",
detail: format!(
"view structural mismatch: original {} nodes/{} arcs, after round-trip {} nodes/{} arcs",
view.node_count(),
view.arc_count(),
view2.node_count(),
view2.arc_count()
),
});
}
if matches!(kind, OpticKind::Prism | OpticKind::Affine) {
check_prism_preview_stability(kind, lens, &view, &restored)?;
}
if matches!(kind, OpticKind::Traversal | OpticKind::Affine) {
check_traversal_putput(kind, lens, &view, &complement)?;
}
if kind == OpticKind::Iso {
check_iso_no_data_loss(kind, &complement)?;
}
Ok(())
}
fn check_prism_preview_stability(
kind: OpticKind,
lens: &crate::Lens,
view: &panproto_inst::WInstance,
restored: &panproto_inst::WInstance,
) -> Result<(), OpticLawViolation> {
use crate::asymmetric::get;
let (view_again, _) = get(lens, restored).map_err(|e| OpticLawViolation {
kind,
law: "Prism preview stability",
detail: format!("re-get failed: {e}"),
})?;
if !crate::laws::instances_equivalent(view, &view_again) {
return Err(OpticLawViolation {
kind,
law: "Prism preview stability",
detail: format!(
"view drifted across `put`/`get`/`put` cycle: {} vs {} nodes",
view.node_count(),
view_again.node_count(),
),
});
}
Ok(())
}
fn check_traversal_putput(
kind: OpticKind,
lens: &crate::Lens,
view: &panproto_inst::WInstance,
complement: &crate::asymmetric::Complement,
) -> Result<(), OpticLawViolation> {
use crate::asymmetric::{get, put};
let perturbed = perturb_view_for_traversal(view);
if crate::laws::instances_equivalent(view, &perturbed) {
return Ok(());
}
let restored1 = put(lens, &perturbed, complement).map_err(|e| OpticLawViolation {
kind,
law: "Traversal PutPut",
detail: format!("first put failed: {e}"),
})?;
let (view_after, _) = get(lens, &restored1).map_err(|e| OpticLawViolation {
kind,
law: "Traversal PutPut",
detail: format!("get after put failed: {e}"),
})?;
if !crate::laws::instances_equivalent(&perturbed, &view_after) {
return Err(OpticLawViolation {
kind,
law: "Traversal PutPut",
detail: "perturbed view did not round-trip".into(),
});
}
Ok(())
}
fn check_iso_no_data_loss(
kind: OpticKind,
complement: &crate::asymmetric::Complement,
) -> Result<(), OpticLawViolation> {
let has_data_loss = !complement.dropped_nodes.is_empty()
|| !complement.dropped_arcs.is_empty()
|| !complement.dropped_fans.is_empty()
|| !complement.original_extra_fields.is_empty()
|| !complement.original_values.is_empty()
|| !complement.synthesized_nodes.is_empty();
if has_data_loss {
return Err(OpticLawViolation {
kind,
law: "Iso complement must have no data loss",
detail: format!(
"complement has {} dropped nodes, {} dropped arcs, {} dropped fans, \
{} original extra fields, {} original values, {} synthesized nodes",
complement.dropped_nodes.len(),
complement.dropped_arcs.len(),
complement.dropped_fans.len(),
complement.original_extra_fields.len(),
complement.original_values.len(),
complement.synthesized_nodes.len(),
),
});
}
Ok(())
}
fn perturb_view_for_traversal(view: &panproto_inst::WInstance) -> panproto_inst::WInstance {
use panproto_inst::value::FieldPresence;
let mut perturbed = view.clone();
for node in perturbed.nodes.values_mut() {
if let Some(FieldPresence::Present(value)) = node.value.as_mut() {
perturb_value(value);
}
for v in node.extra_fields.values_mut() {
perturb_value(v);
}
}
perturbed
}
fn perturb_value(value: &mut panproto_inst::value::Value) {
use panproto_inst::value::Value;
match value {
Value::Str(s) | Value::CidLink(s) | Value::Token(s) => s.push_str("_t"),
Value::Int(i) => *i = i.wrapping_add(1),
Value::Float(f) => *f += 1.0,
Value::Bool(b) => *b = !*b,
Value::Bytes(b) => b.push(0xFF),
Value::Blob { ref_, .. } => ref_.push_str("_t"),
Value::List(items) => {
if let Some(first) = items.first_mut() {
perturb_value(first);
}
}
Value::Unknown(m) | Value::Opaque { fields: m, .. } => {
if let Some(first) = m.values_mut().next() {
perturb_value(first);
}
}
Value::Null => {}
}
}
#[derive(Debug)]
pub struct OpticLawViolation {
pub kind: OpticKind,
pub law: &'static str,
pub detail: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iso_is_identity_element() {
for kind in all_kinds() {
assert_eq!(OpticKind::Iso.compose(kind), kind);
assert_eq!(kind.compose(OpticKind::Iso), kind);
}
}
#[test]
fn traversal_absorbs_everything() {
for kind in all_kinds() {
assert_eq!(OpticKind::Traversal.compose(kind), OpticKind::Traversal);
assert_eq!(kind.compose(OpticKind::Traversal), OpticKind::Traversal);
}
}
#[test]
fn lens_compose_lens_is_lens() {
assert_eq!(OpticKind::Lens.compose(OpticKind::Lens), OpticKind::Lens);
}
#[test]
fn prism_compose_prism_is_prism() {
assert_eq!(OpticKind::Prism.compose(OpticKind::Prism), OpticKind::Prism);
}
#[test]
fn lens_and_prism_yield_affine() {
assert_eq!(OpticKind::Lens.compose(OpticKind::Prism), OpticKind::Affine);
assert_eq!(OpticKind::Prism.compose(OpticKind::Lens), OpticKind::Affine);
}
#[test]
fn affine_stays_affine_with_lens_prism_affine() {
assert_eq!(
OpticKind::Affine.compose(OpticKind::Lens),
OpticKind::Affine
);
assert_eq!(
OpticKind::Affine.compose(OpticKind::Prism),
OpticKind::Affine
);
assert_eq!(
OpticKind::Affine.compose(OpticKind::Affine),
OpticKind::Affine
);
assert_eq!(
OpticKind::Lens.compose(OpticKind::Affine),
OpticKind::Affine
);
assert_eq!(
OpticKind::Prism.compose(OpticKind::Affine),
OpticKind::Affine
);
}
#[test]
fn composition_is_commutative() {
for a in all_kinds() {
for b in all_kinds() {
assert_eq!(
a.compose(b),
b.compose(a),
"compose should be commutative: {a:?} + {b:?}"
);
}
}
}
#[test]
fn composition_is_associative() {
for a in all_kinds() {
for b in all_kinds() {
for c in all_kinds() {
assert_eq!(
a.compose(b).compose(c),
a.compose(b.compose(c)),
"compose should be associative: ({a:?} + {b:?}) + {c:?}"
);
}
}
}
}
#[test]
fn classify_identity_is_iso() {
assert_eq!(
classify_transform(&TheoryTransform::Identity),
OpticKind::Iso
);
}
#[test]
fn classify_rename_sort_is_iso() {
let t = TheoryTransform::RenameSort {
old: "foo".into(),
new: "bar".into(),
};
assert_eq!(classify_transform(&t), OpticKind::Iso);
}
#[test]
fn classify_rename_op_is_iso() {
let t = TheoryTransform::RenameOp {
old: "f".into(),
new: "g".into(),
};
assert_eq!(classify_transform(&t), OpticKind::Iso);
}
#[test]
fn classify_drop_sort_is_lens() {
let t = TheoryTransform::DropSort("x".into());
assert_eq!(classify_transform(&t), OpticKind::Lens);
}
#[test]
fn classify_drop_op_is_lens() {
let t = TheoryTransform::DropOp("f".into());
assert_eq!(classify_transform(&t), OpticKind::Lens);
}
#[test]
fn classify_add_sort_is_lens() {
let t = TheoryTransform::AddSort {
sort: panproto_gat::Sort::simple("new_sort"),
vertex_kind: None,
};
assert_eq!(classify_transform(&t), OpticKind::Lens);
}
#[test]
fn classify_compose_two_isos_is_iso() {
let t = TheoryTransform::Compose(
Box::new(TheoryTransform::RenameSort {
old: "a".into(),
new: "b".into(),
}),
Box::new(TheoryTransform::RenameOp {
old: "f".into(),
new: "g".into(),
}),
);
assert_eq!(classify_transform(&t), OpticKind::Iso);
}
#[test]
fn classify_compose_iso_and_lens_is_lens() {
let t = TheoryTransform::Compose(
Box::new(TheoryTransform::RenameSort {
old: "a".into(),
new: "b".into(),
}),
Box::new(TheoryTransform::DropSort("x".into())),
);
assert_eq!(classify_transform(&t), OpticKind::Lens);
}
#[test]
fn classify_compose_two_lenses_is_lens() {
let t = TheoryTransform::Compose(
Box::new(TheoryTransform::DropSort("x".into())),
Box::new(TheoryTransform::DropOp("f".into())),
);
assert_eq!(classify_transform(&t), OpticKind::Lens);
}
#[test]
fn optic_kind_serde_round_trip() {
for kind in all_kinds() {
let json =
serde_json::to_string(&kind).unwrap_or_else(|e| panic!("serialize {kind:?}: {e}"));
let back: OpticKind =
serde_json::from_str(&json).unwrap_or_else(|e| panic!("deserialize {kind:?}: {e}"));
assert_eq!(kind, back);
}
}
#[test]
fn identity_lens_satisfies_iso_laws() {
use crate::tests::{identity_lens, three_node_instance, three_node_schema};
let schema = three_node_schema();
let lens = identity_lens(&schema);
let instance = three_node_instance();
let result = check_optic_laws(OpticKind::Iso, &lens, &instance);
assert!(
result.is_ok(),
"identity lens should satisfy Iso laws: {result:?}"
);
}
#[test]
fn projection_lens_satisfies_lens_laws() {
use crate::tests::{projection_lens, three_node_instance, three_node_schema};
let schema = three_node_schema();
let lens = projection_lens(&schema, "text");
let instance = three_node_instance();
let result = check_optic_laws(OpticKind::Lens, &lens, &instance);
assert!(
result.is_ok(),
"projection lens should satisfy Lens laws: {result:?}"
);
}
fn all_kinds() -> [OpticKind; 5] {
[
OpticKind::Iso,
OpticKind::Lens,
OpticKind::Prism,
OpticKind::Affine,
OpticKind::Traversal,
]
}
}