diff --git a/src/lax/category.rs b/src/lax/category.rs index f75d4b3..956561c 100644 --- a/src/lax/category.rs +++ b/src/lax/category.rs @@ -32,6 +32,18 @@ impl Arrow for OpenHypergraph { if self.target() != other.source() { return None; } + self.lax_compose(other) + } +} + +impl OpenHypergraph { + /// Compose two open hypergraphs, unifying the boundary nodes without checking labels. + /// + /// Returns None when the boundary arities do not match. + pub fn lax_compose(&self, other: &Self) -> Option { + if self.targets.len() != other.sources.len() { + return None; + } let n = self.hypergraph.nodes.len(); let mut f = self.tensor(other); @@ -39,7 +51,7 @@ impl Arrow for OpenHypergraph { f.unify(*u, NodeId(v.0 + n)); } - f.sources = f.sources[..self.sources.len()].to_vec(); + f.sources.truncate(self.sources.len()); f.targets = f.targets[self.targets.len()..].to_vec(); Some(f) diff --git a/tests/lax/mod.rs b/tests/lax/mod.rs index a3a11c8..2e1d7c6 100644 --- a/tests/lax/mod.rs +++ b/tests/lax/mod.rs @@ -1,2 +1,3 @@ pub mod eval; pub mod hypergraph; +pub mod open_hypergraph; diff --git a/tests/lax/open_hypergraph.rs b/tests/lax/open_hypergraph.rs new file mode 100644 index 0000000..188f033 --- /dev/null +++ b/tests/lax/open_hypergraph.rs @@ -0,0 +1,47 @@ +use open_hypergraphs::category::Arrow; +use open_hypergraphs::lax::{NodeId, OpenHypergraph}; + +#[derive(Clone, Debug, PartialEq)] +enum Obj { + A, + B, + C, +} + +#[derive(Clone, Debug, PartialEq)] +enum Op { + F, + G, +} + +#[test] +fn test_compose_type_mismatch() { + let f = OpenHypergraph::singleton(Op::F, vec![Obj::A], vec![Obj::B]); + let g = OpenHypergraph::singleton(Op::G, vec![Obj::C], vec![Obj::A]); + + assert!(Arrow::compose(&f, &g).is_none()); +} + +#[test] +fn test_lax_compose_allows_mismatch_and_unifies_boundary() { + let f = OpenHypergraph::singleton(Op::F, vec![Obj::A], vec![Obj::B]); + let g = OpenHypergraph::singleton(Op::G, vec![Obj::C], vec![Obj::A]); + + let composed = f.lax_compose(&g).expect("arity matches"); + + assert_eq!(composed.sources, vec![NodeId(0)]); + assert_eq!(composed.targets, vec![NodeId(3)]); + assert_eq!(composed.hypergraph.quotient.0, vec![NodeId(1)]); + assert_eq!(composed.hypergraph.quotient.1, vec![NodeId(2)]); +} + +#[test] +fn test_compose_matches_lax_compose_on_equal_boundaries() { + let f = OpenHypergraph::singleton(Op::F, vec![Obj::A], vec![Obj::B]); + let g = OpenHypergraph::singleton(Op::G, vec![Obj::B], vec![Obj::C]); + + let strict = Arrow::compose(&f, &g).expect("labels match"); + let lax = f.lax_compose(&g).expect("arity matches"); + + assert_eq!(strict, lax); +}