1use std::sync::Arc;
2
3use rustc_hash::FxHashMap;
4
5use crate::eq::Term;
6
7#[derive(Debug, Clone, Eq, serde::Serialize)]
29#[serde(untagged)]
30pub enum SortExpr {
31 Name(Arc<str>),
33 App {
35 name: Arc<str>,
37 args: Vec<Term>,
39 },
40}
41
42impl SortExpr {
43 #[must_use]
50 pub fn app(name: impl Into<Arc<str>>, args: Vec<Term>) -> Self {
51 if args.is_empty() {
52 Self::Name(name.into())
53 } else {
54 Self::App {
55 name: name.into(),
56 args,
57 }
58 }
59 }
60
61 #[must_use]
67 pub fn normalize(self) -> Self {
68 if let Self::App { name, args } = &self {
69 if args.is_empty() {
70 return Self::Name(Arc::clone(name));
71 }
72 }
73 self
74 }
75
76 #[must_use]
78 pub const fn head(&self) -> &Arc<str> {
79 match self {
80 Self::Name(n) | Self::App { name: n, .. } => n,
81 }
82 }
83
84 #[must_use]
86 pub fn args(&self) -> &[Term] {
87 match self {
88 Self::Name(_) => &[],
89 Self::App { args, .. } => args,
90 }
91 }
92
93 #[must_use]
96 pub fn subst(&self, mapping: &FxHashMap<Arc<str>, Term>) -> Self {
97 match self {
98 Self::Name(n) => Self::Name(Arc::clone(n)),
99 Self::App { name, args } => Self::app(
100 Arc::clone(name),
101 args.iter().map(|t| t.substitute(mapping)).collect(),
102 ),
103 }
104 }
105
106 #[must_use]
112 pub fn alpha_eq(&self, other: &Self) -> bool {
113 self.head() == other.head() && self.args() == other.args()
114 }
115
116 #[must_use]
126 pub fn alpha_eq_modulo_rewrites(
127 &self,
128 other: &Self,
129 rules: &[crate::eq::DirectedEquation],
130 step_limit: usize,
131 ) -> bool {
132 if self.head() != other.head() {
133 return false;
134 }
135 if self.args().len() != other.args().len() {
136 return false;
137 }
138 let normalize_all = |args: &[Term]| -> Vec<Term> {
139 args.iter()
140 .map(|t| crate::eq::normalize(t, rules, step_limit))
141 .collect()
142 };
143 let left = normalize_all(self.args());
144 let right = normalize_all(other.args());
145 left == right
146 }
147
148 #[must_use]
151 pub fn rename_head(&self, sort_map: &std::collections::HashMap<Arc<str>, Arc<str>>) -> Self {
152 match self {
153 Self::Name(n) => Self::Name(sort_map.get(n).cloned().unwrap_or_else(|| Arc::clone(n))),
154 Self::App { name, args } => Self::app(
155 sort_map
156 .get(name)
157 .cloned()
158 .unwrap_or_else(|| Arc::clone(name)),
159 args.clone(),
160 ),
161 }
162 }
163
164 #[must_use]
167 pub fn apply_maps(
168 &self,
169 sort_map: &std::collections::HashMap<Arc<str>, Arc<str>>,
170 op_map: &std::collections::HashMap<Arc<str>, Arc<str>>,
171 ) -> Self {
172 match self {
173 Self::Name(n) => Self::Name(sort_map.get(n).cloned().unwrap_or_else(|| Arc::clone(n))),
174 Self::App { name, args } => Self::app(
175 sort_map
176 .get(name)
177 .cloned()
178 .unwrap_or_else(|| Arc::clone(name)),
179 args.iter().map(|t| t.rename_ops(op_map)).collect(),
180 ),
181 }
182 }
183}
184
185#[must_use]
198pub fn positional_param_rename<I, J>(
199 domain_params: I,
200 target_params: J,
201) -> FxHashMap<Arc<str>, Term>
202where
203 I: IntoIterator<Item = Arc<str>>,
204 J: IntoIterator<Item = Arc<str>>,
205{
206 let mut rename = FxHashMap::default();
207 for (d, t) in domain_params.into_iter().zip(target_params) {
208 if d != t {
209 rename.insert(d, Term::Var(t));
210 }
211 }
212 rename
213}
214
215#[must_use]
228pub fn signatures_equivalent_modulo_param_rename(
229 lhs_inputs: &[(Arc<str>, SortExpr, crate::op::Implicit)],
230 lhs_output: &SortExpr,
231 rhs_inputs: &[(Arc<str>, SortExpr, crate::op::Implicit)],
232 rhs_output: &SortExpr,
233) -> bool {
234 if lhs_inputs.len() != rhs_inputs.len() {
235 return false;
236 }
237 let rename = positional_param_rename(
238 lhs_inputs.iter().map(|(n, _, _)| Arc::clone(n)),
239 rhs_inputs.iter().map(|(n, _, _)| Arc::clone(n)),
240 );
241 for ((_, lhs_sort, l_imp), (_, rhs_sort, r_imp)) in lhs_inputs.iter().zip(rhs_inputs.iter()) {
242 if l_imp != r_imp {
243 return false;
244 }
245 if !lhs_sort.subst(&rename).alpha_eq(rhs_sort) {
246 return false;
247 }
248 }
249 lhs_output.subst(&rename).alpha_eq(rhs_output)
250}
251
252#[must_use]
259pub fn sort_params_equivalent_modulo_rename(lhs: &[SortParam], rhs: &[SortParam]) -> bool {
260 if lhs.len() != rhs.len() {
261 return false;
262 }
263 let rename = positional_param_rename(
264 lhs.iter().map(|p| Arc::clone(&p.name)),
265 rhs.iter().map(|p| Arc::clone(&p.name)),
266 );
267 lhs.iter()
268 .zip(rhs.iter())
269 .all(|(l, r)| l.sort.subst(&rename).alpha_eq(&r.sort))
270}
271
272impl PartialEq for SortExpr {
273 fn eq(&self, other: &Self) -> bool {
278 self.head() == other.head() && self.args() == other.args()
279 }
280}
281
282impl std::hash::Hash for SortExpr {
283 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
287 self.head().hash(state);
288 self.args().hash(state);
289 }
290}
291
292impl<'de> serde::Deserialize<'de> for SortExpr {
293 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
297 where
298 D: serde::Deserializer<'de>,
299 {
300 #[derive(serde::Deserialize)]
301 #[serde(untagged)]
302 enum Raw {
303 Name(Arc<str>),
304 App { name: Arc<str>, args: Vec<Term> },
305 }
306 match Raw::deserialize(deserializer)? {
307 Raw::Name(n) => Ok(Self::Name(n)),
308 Raw::App { name, args } => Ok(Self::app(name, args)),
309 }
310 }
311}
312
313impl std::fmt::Display for SortExpr {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 match self {
316 Self::Name(n) => f.write_str(n),
317 Self::App { name, args } => {
318 f.write_str(name)?;
319 f.write_str("(")?;
320 for (i, a) in args.iter().enumerate() {
321 if i > 0 {
322 f.write_str(", ")?;
323 }
324 write!(f, "{a}")?;
325 }
326 f.write_str(")")
327 }
328 }
329 }
330}
331
332impl std::fmt::Display for Term {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 match self {
335 Self::Var(n) => f.write_str(n),
336 Self::App { op, args } if args.is_empty() => write!(f, "{op}()"),
337 Self::App { op, args } => {
338 write!(f, "{op}(")?;
339 for (i, a) in args.iter().enumerate() {
340 if i > 0 {
341 f.write_str(", ")?;
342 }
343 write!(f, "{a}")?;
344 }
345 f.write_str(")")
346 }
347 Self::Hole { name } => match name {
348 Some(n) => write!(f, "?{n}"),
349 None => f.write_str("?"),
350 },
351 Self::Let { name, bound, body } => {
352 write!(f, "let {name} = {bound} in {body}")
353 }
354 Self::Case {
355 scrutinee,
356 branches,
357 } => {
358 write!(f, "case {scrutinee} of ")?;
359 for (i, b) in branches.iter().enumerate() {
360 if i > 0 {
361 f.write_str(" | ")?;
362 }
363 write!(f, "{}(", b.constructor)?;
364 for (j, binder) in b.binders.iter().enumerate() {
365 if j > 0 {
366 f.write_str(", ")?;
367 }
368 f.write_str(binder)?;
369 }
370 write!(f, ") => {}", b.body)?;
371 }
372 f.write_str(" end")
373 }
374 }
375 }
376}
377
378impl From<&str> for SortExpr {
379 fn from(s: &str) -> Self {
380 Self::Name(Arc::from(s))
381 }
382}
383
384impl From<String> for SortExpr {
385 fn from(s: String) -> Self {
386 Self::Name(Arc::from(s))
387 }
388}
389
390impl From<Arc<str>> for SortExpr {
391 fn from(s: Arc<str>) -> Self {
392 Self::Name(s)
393 }
394}
395
396impl From<&Arc<str>> for SortExpr {
397 fn from(s: &Arc<str>) -> Self {
398 Self::Name(Arc::clone(s))
399 }
400}
401
402#[derive(
438 Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
439)]
440#[non_exhaustive]
441pub enum CoercionClass {
442 #[default]
445 Iso,
446 Retraction,
451 Projection,
464 Opaque,
467}
468
469impl CoercionClass {
470 #[must_use]
483 pub const fn compose(self, other: Self) -> Self {
484 match (self, other) {
485 (Self::Iso, x) | (x, Self::Iso) => x,
486 (Self::Opaque, _) | (_, Self::Opaque) => Self::Opaque,
487 (Self::Retraction, Self::Retraction) => Self::Retraction,
488 (Self::Projection, Self::Projection) => Self::Projection,
489 (Self::Retraction, Self::Projection) | (Self::Projection, Self::Retraction) => {
493 Self::Opaque
494 }
495 }
496 }
497
498 #[must_use]
505 pub const fn is_lossless(self) -> bool {
506 matches!(self, Self::Iso)
507 }
508
509 #[must_use]
523 pub const fn needs_complement_storage(self) -> bool {
524 matches!(self, Self::Retraction | Self::Opaque)
525 }
526
527 #[must_use]
541 pub const fn all() -> &'static [Self] {
542 const fn _exhaustiveness_witness(c: CoercionClass) {
546 match c {
547 CoercionClass::Iso
548 | CoercionClass::Retraction
549 | CoercionClass::Projection
550 | CoercionClass::Opaque => {}
551 }
552 }
553 const ALL: &[CoercionClass] = &[
554 CoercionClass::Iso,
555 CoercionClass::Retraction,
556 CoercionClass::Projection,
557 CoercionClass::Opaque,
558 ];
559 ALL
560 }
561}
562
563impl PartialOrd for CoercionClass {
564 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
565 Some(self.cmp(other))
566 }
567}
568
569impl Ord for CoercionClass {
570 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
586 const fn rank(c: CoercionClass) -> u8 {
587 match c {
588 CoercionClass::Iso => 0,
589 CoercionClass::Retraction => 1,
590 CoercionClass::Projection => 2,
591 CoercionClass::Opaque => 3,
592 }
593 }
594 rank(*self).cmp(&rank(*other))
595 }
596}
597
598#[must_use]
603pub const fn classify_builtin_coercion(
604 op: panproto_expr::BuiltinOp,
605) -> Option<(ValueKind, ValueKind, CoercionClass)> {
606 use panproto_expr::BuiltinOp;
607 match op {
608 BuiltinOp::IntToFloat => {
611 Some((ValueKind::Int, ValueKind::Float, CoercionClass::Retraction))
612 }
613 BuiltinOp::FloatToInt => Some((ValueKind::Float, ValueKind::Int, CoercionClass::Opaque)),
615 BuiltinOp::IntToStr => Some((ValueKind::Int, ValueKind::Str, CoercionClass::Retraction)),
619 BuiltinOp::FloatToStr => Some((ValueKind::Float, ValueKind::Str, CoercionClass::Opaque)),
622 BuiltinOp::StrToInt => Some((ValueKind::Str, ValueKind::Int, CoercionClass::Opaque)),
624 BuiltinOp::StrToFloat => Some((ValueKind::Str, ValueKind::Float, CoercionClass::Opaque)),
626 _ => None,
627 }
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
632pub enum ValueKind {
633 Bool,
635 Int,
637 Float,
639 Str,
641 Bytes,
643 Token,
645 Null,
647 Any,
649}
650
651impl ValueKind {
652 #[must_use]
654 pub const fn as_str(&self) -> &'static str {
655 match self {
656 Self::Bool => "boolean",
657 Self::Int => "integer",
658 Self::Float => "number",
659 Self::Str => "string",
660 Self::Bytes => "bytes",
661 Self::Token => "token",
662 Self::Null => "null",
663 Self::Any => "any",
664 }
665 }
666
667 #[must_use]
675 pub const fn all() -> &'static [Self] {
676 const fn _exhaustiveness_witness(k: ValueKind) {
679 match k {
680 ValueKind::Bool
681 | ValueKind::Int
682 | ValueKind::Float
683 | ValueKind::Str
684 | ValueKind::Bytes
685 | ValueKind::Token
686 | ValueKind::Null
687 | ValueKind::Any => {}
688 }
689 }
690 const ALL: &[ValueKind] = &[
691 ValueKind::Bool,
692 ValueKind::Int,
693 ValueKind::Float,
694 ValueKind::Str,
695 ValueKind::Bytes,
696 ValueKind::Token,
697 ValueKind::Null,
698 ValueKind::Any,
699 ];
700 ALL
701 }
702}
703
704#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
706pub enum SortKind {
707 #[default]
709 Structural,
710 Val(ValueKind),
712 Coercion {
715 from: ValueKind,
717 to: ValueKind,
719 class: CoercionClass,
721 },
722 Merger(ValueKind),
724}
725
726#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
736pub struct SortParam {
737 pub name: Arc<str>,
739 pub sort: SortExpr,
742}
743
744#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
751pub enum SortClosure {
752 #[default]
755 Open,
756 Closed(Vec<Arc<str>>),
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
778pub struct Sort {
779 pub name: Arc<str>,
781 pub params: Vec<SortParam>,
783 #[serde(default)]
785 pub kind: SortKind,
786 #[serde(default)]
788 pub closure: SortClosure,
789}
790
791impl Sort {
792 #[must_use]
795 pub fn simple(name: impl Into<Arc<str>>) -> Self {
796 Self {
797 name: name.into(),
798 params: Vec::new(),
799 kind: SortKind::default(),
800 closure: SortClosure::Open,
801 }
802 }
803
804 #[must_use]
807 pub fn dependent(name: impl Into<Arc<str>>, params: Vec<SortParam>) -> Self {
808 Self {
809 name: name.into(),
810 params,
811 kind: SortKind::default(),
812 closure: SortClosure::Open,
813 }
814 }
815
816 #[must_use]
818 pub fn with_kind(name: impl Into<Arc<str>>, kind: SortKind) -> Self {
819 Self {
820 name: name.into(),
821 params: Vec::new(),
822 kind,
823 closure: SortClosure::Open,
824 }
825 }
826
827 #[must_use]
835 pub fn closed<I, S>(name: impl Into<Arc<str>>, params: Vec<SortParam>, constructors: I) -> Self
836 where
837 I: IntoIterator<Item = S>,
838 S: Into<Arc<str>>,
839 {
840 Self {
841 name: name.into(),
842 params,
843 kind: SortKind::default(),
844 closure: SortClosure::Closed(constructors.into_iter().map(Into::into).collect()),
845 }
846 }
847
848 #[must_use]
854 pub fn default_vertex_kind(&self) -> Arc<str> {
855 match &self.kind {
856 SortKind::Val(vk) => Arc::from(vk.as_str()),
857 SortKind::Structural | SortKind::Coercion { .. } | SortKind::Merger(_) => {
858 Arc::clone(&self.name)
859 }
860 }
861 }
862
863 #[must_use]
865 pub fn is_simple(&self) -> bool {
866 self.params.is_empty()
867 }
868
869 #[must_use]
871 pub fn arity(&self) -> usize {
872 self.params.len()
873 }
874}
875
876impl SortParam {
877 #[must_use]
883 pub fn new(name: impl Into<Arc<str>>, sort: impl Into<SortExpr>) -> Self {
884 Self {
885 name: name.into(),
886 sort: sort.into(),
887 }
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894
895 #[test]
896 fn simple_sort() {
897 let s = Sort::simple("Vertex");
898 assert!(s.is_simple());
899 assert_eq!(s.arity(), 0);
900 assert_eq!(&*s.name, "Vertex");
901 }
902
903 #[test]
904 fn dependent_sort() {
905 let s = Sort::dependent(
906 "Hom",
907 vec![SortParam::new("a", "Ob"), SortParam::new("b", "Ob")],
908 );
909 assert!(!s.is_simple());
910 assert_eq!(s.arity(), 2);
911 }
912
913 #[test]
916 fn sort_expr_from_str() {
917 let e: SortExpr = "Ob".into();
918 assert_eq!(e, SortExpr::Name(Arc::from("Ob")));
919 assert_eq!(&**e.head(), "Ob");
920 }
921
922 #[test]
923 fn sort_expr_app_head() {
924 let e = SortExpr::App {
925 name: Arc::from("Hom"),
926 args: vec![Term::var("x"), Term::var("y")],
927 };
928 assert_eq!(&**e.head(), "Hom");
929 assert_eq!(e.args().len(), 2);
930 }
931
932 #[test]
933 fn sort_expr_alpha_eq_name_vs_empty_app() {
934 let a = SortExpr::Name(Arc::from("Ob"));
935 let b = SortExpr::App {
936 name: Arc::from("Ob"),
937 args: Vec::new(),
938 };
939 assert!(a.alpha_eq(&b));
940 assert!(b.alpha_eq(&a));
941 }
942
943 #[test]
944 fn sort_expr_alpha_eq_structural() {
945 let a = SortExpr::App {
946 name: Arc::from("Hom"),
947 args: vec![Term::var("x"), Term::var("y")],
948 };
949 let b = SortExpr::App {
950 name: Arc::from("Hom"),
951 args: vec![Term::var("x"), Term::var("y")],
952 };
953 let c = SortExpr::App {
954 name: Arc::from("Hom"),
955 args: vec![Term::var("y"), Term::var("x")],
956 };
957 assert!(a.alpha_eq(&b));
958 assert!(!a.alpha_eq(&c));
959 }
960
961 #[test]
962 fn sort_expr_subst() {
963 let e = SortExpr::App {
964 name: Arc::from("Hom"),
965 args: vec![Term::var("x"), Term::var("y")],
966 };
967 let mut mapping: FxHashMap<Arc<str>, Term> = FxHashMap::default();
968 mapping.insert(Arc::from("x"), Term::constant("a"));
969 mapping.insert(Arc::from("y"), Term::constant("b"));
970 let result = e.subst(&mapping);
971 assert_eq!(
972 result,
973 SortExpr::App {
974 name: Arc::from("Hom"),
975 args: vec![Term::constant("a"), Term::constant("b")],
976 }
977 );
978 }
979
980 #[test]
981 fn sort_expr_subst_name_unchanged() {
982 let e = SortExpr::Name(Arc::from("Ob"));
983 let mut mapping: FxHashMap<Arc<str>, Term> = FxHashMap::default();
984 mapping.insert(Arc::from("x"), Term::constant("a"));
985 assert_eq!(e.subst(&mapping), e);
986 }
987
988 #[test]
989 fn sort_expr_serde_name_is_bare_string() -> Result<(), Box<dyn std::error::Error>> {
990 let e = SortExpr::Name(Arc::from("Ob"));
991 let s = serde_json::to_string(&e)?;
992 assert_eq!(s, "\"Ob\"");
993 let back: SortExpr = serde_json::from_str(&s)?;
994 assert_eq!(back, e);
995 Ok(())
996 }
997
998 #[test]
999 fn sort_expr_serde_app_is_struct() -> Result<(), Box<dyn std::error::Error>> {
1000 let e = SortExpr::App {
1001 name: Arc::from("Hom"),
1002 args: vec![Term::var("x"), Term::var("y")],
1003 };
1004 let s = serde_json::to_string(&e)?;
1005 let back: SortExpr = serde_json::from_str(&s)?;
1006 assert_eq!(back, e);
1007 Ok(())
1008 }
1009
1010 #[test]
1011 fn sort_expr_display() {
1012 let e = SortExpr::App {
1013 name: Arc::from("Hom"),
1014 args: vec![Term::var("x"), Term::var("y")],
1015 };
1016 assert_eq!(format!("{e}"), "Hom(x, y)");
1017 let n = SortExpr::Name(Arc::from("Ob"));
1018 assert_eq!(format!("{n}"), "Ob");
1019 }
1020
1021 const ALL_CLASSES: [CoercionClass; 4] = [
1024 CoercionClass::Iso,
1025 CoercionClass::Retraction,
1026 CoercionClass::Projection,
1027 CoercionClass::Opaque,
1028 ];
1029
1030 #[test]
1031 fn coercion_class_identity() {
1032 for &x in &ALL_CLASSES {
1033 assert_eq!(CoercionClass::Iso.compose(x), x, "Iso is left identity");
1034 assert_eq!(x.compose(CoercionClass::Iso), x, "Iso is right identity");
1035 }
1036 }
1037
1038 #[test]
1039 fn coercion_class_absorption() {
1040 for &x in &ALL_CLASSES {
1041 assert_eq!(
1042 CoercionClass::Opaque.compose(x),
1043 CoercionClass::Opaque,
1044 "Opaque absorbs on left"
1045 );
1046 assert_eq!(
1047 x.compose(CoercionClass::Opaque),
1048 CoercionClass::Opaque,
1049 "Opaque absorbs on right"
1050 );
1051 }
1052 }
1053
1054 #[test]
1055 fn coercion_class_associativity() {
1056 for &a in &ALL_CLASSES {
1057 for &b in &ALL_CLASSES {
1058 for &c in &ALL_CLASSES {
1059 assert_eq!(
1060 a.compose(b).compose(c),
1061 a.compose(b.compose(c)),
1062 "associativity: ({a:?} . {b:?}) . {c:?} == {a:?} . ({b:?} . {c:?})"
1063 );
1064 }
1065 }
1066 }
1067 }
1068
1069 #[test]
1070 fn coercion_class_commutativity() {
1071 for &a in &ALL_CLASSES {
1072 for &b in &ALL_CLASSES {
1073 assert_eq!(
1074 a.compose(b),
1075 b.compose(a),
1076 "commutativity: {a:?} . {b:?} == {b:?} . {a:?}"
1077 );
1078 }
1079 }
1080 }
1081
1082 #[test]
1083 fn coercion_class_ordering_consistent_with_compose() {
1084 for &a in &ALL_CLASSES {
1085 for &b in &ALL_CLASSES {
1086 let composed = a.compose(b);
1087 assert!(
1088 composed >= a,
1089 "compose({a:?}, {b:?}) = {composed:?} should be >= {a:?}"
1090 );
1091 assert!(
1092 composed >= b,
1093 "compose({a:?}, {b:?}) = {composed:?} should be >= {b:?}"
1094 );
1095 }
1096 }
1097 }
1098
1099 #[test]
1100 fn classify_builtin_coercion_coverage() {
1101 use panproto_expr::BuiltinOp;
1102
1103 let coercion_ops = [
1105 BuiltinOp::IntToFloat,
1106 BuiltinOp::FloatToInt,
1107 BuiltinOp::IntToStr,
1108 BuiltinOp::FloatToStr,
1109 BuiltinOp::StrToInt,
1110 BuiltinOp::StrToFloat,
1111 ];
1112 for op in coercion_ops {
1113 assert!(
1114 classify_builtin_coercion(op).is_some(),
1115 "{op:?} should be classified"
1116 );
1117 }
1118
1119 assert!(classify_builtin_coercion(BuiltinOp::Add).is_none());
1121 assert!(classify_builtin_coercion(BuiltinOp::Concat).is_none());
1122 }
1123
1124 #[test]
1125 fn no_builtin_classified_as_iso() {
1126 use panproto_expr::BuiltinOp;
1127
1128 let coercion_ops = [
1130 BuiltinOp::IntToFloat,
1131 BuiltinOp::FloatToInt,
1132 BuiltinOp::IntToStr,
1133 BuiltinOp::FloatToStr,
1134 BuiltinOp::StrToInt,
1135 BuiltinOp::StrToFloat,
1136 ];
1137 for op in coercion_ops {
1138 if let Some((_, _, class)) = classify_builtin_coercion(op) {
1139 assert_ne!(
1140 class,
1141 CoercionClass::Iso,
1142 "{op:?} should not be classified as Iso"
1143 );
1144 }
1145 }
1146 }
1147
1148 #[test]
1149 fn needs_complement_storage_consistent_with_lattice() {
1150 assert!(
1155 !CoercionClass::Iso.needs_complement_storage(),
1156 "Iso: lossless, no storage"
1157 );
1158 assert!(
1159 CoercionClass::Retraction.needs_complement_storage(),
1160 "Retraction: stores residual"
1161 );
1162 assert!(
1163 !CoercionClass::Projection.needs_complement_storage(),
1164 "Projection: derived value re-computed, no storage"
1165 );
1166 assert!(
1167 CoercionClass::Opaque.needs_complement_storage(),
1168 "Opaque: stores entire original"
1169 );
1170 }
1171
1172 #[test]
1173 fn projection_compose_laws() {
1174 assert_eq!(
1179 CoercionClass::Projection.compose(CoercionClass::Projection),
1180 CoercionClass::Projection,
1181 "Projection . Projection = Projection (projections compose)"
1182 );
1183
1184 assert_eq!(
1188 CoercionClass::Retraction.compose(CoercionClass::Projection),
1189 CoercionClass::Opaque,
1190 "Retraction . Projection = Opaque (diamond lattice meet)"
1191 );
1192 assert_eq!(
1193 CoercionClass::Projection.compose(CoercionClass::Retraction),
1194 CoercionClass::Opaque,
1195 "Projection . Retraction = Opaque (commutativity of meet)"
1196 );
1197
1198 assert_eq!(
1200 CoercionClass::Iso.compose(CoercionClass::Projection),
1201 CoercionClass::Projection,
1202 );
1203
1204 assert_eq!(
1206 CoercionClass::Opaque.compose(CoercionClass::Projection),
1207 CoercionClass::Opaque,
1208 );
1209 }
1210
1211 #[test]
1214 fn empty_args_app_normalizes_to_name() {
1215 let raw = SortExpr::App {
1216 name: Arc::from("Ob"),
1217 args: Vec::new(),
1218 };
1219 let n = raw.normalize();
1220 assert!(matches!(n, SortExpr::Name(ref s) if &**s == "Ob"));
1221 assert_eq!(n.clone().normalize(), n);
1223 }
1224
1225 #[test]
1226 fn smart_constructor_collapses_empty_args() {
1227 let v = SortExpr::app("Ob", Vec::new());
1228 assert!(matches!(v, SortExpr::Name(_)));
1229 }
1230
1231 #[test]
1232 fn smart_constructor_preserves_nonempty() {
1233 let v = SortExpr::app("Hom", vec![Term::var("x"), Term::var("y")]);
1234 assert!(matches!(v, SortExpr::App { .. }));
1235 }
1236
1237 #[test]
1238 fn eq_treats_name_and_empty_app_equal() {
1239 let a = SortExpr::Name(Arc::from("Ob"));
1240 let b = SortExpr::App {
1241 name: Arc::from("Ob"),
1242 args: Vec::new(),
1243 };
1244 assert_eq!(a, b);
1245 }
1246
1247 #[test]
1248 fn hash_agrees_with_eq_across_spellings() {
1249 use std::collections::hash_map::DefaultHasher;
1250 use std::hash::{Hash, Hasher};
1251
1252 let a = SortExpr::Name(Arc::from("Ob"));
1253 let b = SortExpr::App {
1254 name: Arc::from("Ob"),
1255 args: Vec::new(),
1256 };
1257 let hash = |v: &SortExpr| {
1258 let mut h = DefaultHasher::new();
1259 v.hash(&mut h);
1260 h.finish()
1261 };
1262 assert_eq!(hash(&a), hash(&b));
1263 }
1264
1265 #[test]
1266 fn hashmap_lookup_crosses_spellings() {
1267 let mut m: FxHashMap<SortExpr, usize> = FxHashMap::default();
1268 m.insert(SortExpr::Name(Arc::from("Ob")), 1);
1269 let key = SortExpr::App {
1270 name: Arc::from("Ob"),
1271 args: Vec::new(),
1272 };
1273 assert_eq!(m.get(&key).copied(), Some(1));
1274 }
1275
1276 #[test]
1277 fn subst_produces_normalized_output() {
1278 let e = SortExpr::App {
1279 name: Arc::from("S"),
1280 args: vec![Term::var("x")],
1281 };
1282 let mut mapping: FxHashMap<Arc<str>, Term> = FxHashMap::default();
1283 mapping.insert(Arc::from("x"), Term::constant("c"));
1287 let r = e.subst(&mapping);
1288 assert!(matches!(r, SortExpr::App { .. }));
1289 }
1290
1291 #[test]
1292 fn rename_head_normalizes_empty_app() {
1293 let e = SortExpr::App {
1294 name: Arc::from("Ob"),
1295 args: Vec::new(),
1296 };
1297 let mut sm: std::collections::HashMap<Arc<str>, Arc<str>> =
1298 std::collections::HashMap::new();
1299 sm.insert(Arc::from("Ob"), Arc::from("Obj"));
1300 let r = e.rename_head(&sm);
1301 assert!(matches!(r, SortExpr::Name(ref n) if &**n == "Obj"));
1302 }
1303
1304 #[test]
1305 fn deserialize_empty_args_app_normalizes() -> Result<(), Box<dyn std::error::Error>> {
1306 let json = r#"{"name":"Ob","args":[]}"#;
1307 let v: SortExpr = serde_json::from_str(json)?;
1308 assert!(matches!(v, SortExpr::Name(ref n) if &**n == "Ob"));
1309 Ok(())
1310 }
1311
1312 #[test]
1315 fn positional_rename_identity_is_empty() {
1316 let r = positional_param_rename(
1317 [Arc::from("a"), Arc::from("b")],
1318 [Arc::from("a"), Arc::from("b")],
1319 );
1320 assert!(r.is_empty(), "identity rename should be empty");
1321 }
1322
1323 #[test]
1324 fn positional_rename_maps_differing_names_only() {
1325 let r = positional_param_rename(
1326 [Arc::from("a"), Arc::from("y"), Arc::from("c")],
1327 [Arc::from("x"), Arc::from("y"), Arc::from("z")],
1328 );
1329 assert_eq!(r.len(), 2);
1330 assert_eq!(r.get(&Arc::from("a")), Some(&Term::var("x")));
1331 assert_eq!(r.get(&Arc::from("c")), Some(&Term::var("z")));
1332 assert!(!r.contains_key(&Arc::from("y")));
1333 }
1334
1335 #[test]
1336 fn signature_equivalence_accepts_alpha_variant() {
1337 use crate::op::Implicit;
1338 let lhs_inputs = vec![(Arc::from("a"), SortExpr::from("Ob"), Implicit::No)];
1340 let lhs_output = SortExpr::App {
1341 name: Arc::from("Hom"),
1342 args: vec![Term::var("a"), Term::var("a")],
1343 };
1344 let rhs_inputs = vec![(Arc::from("x"), SortExpr::from("Ob"), Implicit::No)];
1345 let rhs_output = SortExpr::App {
1346 name: Arc::from("Hom"),
1347 args: vec![Term::var("x"), Term::var("x")],
1348 };
1349 assert!(signatures_equivalent_modulo_param_rename(
1350 &lhs_inputs,
1351 &lhs_output,
1352 &rhs_inputs,
1353 &rhs_output,
1354 ));
1355 }
1356
1357 #[test]
1358 fn signature_equivalence_rejects_swap() {
1359 use crate::op::Implicit;
1360 let hom = |a: &str, b: &str| SortExpr::App {
1362 name: Arc::from("Hom"),
1363 args: vec![Term::var(a), Term::var(b)],
1364 };
1365 let lhs_inputs = vec![
1366 (Arc::from("x"), SortExpr::from("Ob"), Implicit::No),
1367 (Arc::from("y"), SortExpr::from("Ob"), Implicit::No),
1368 ];
1369 let rhs_inputs = lhs_inputs.clone();
1370 assert!(!signatures_equivalent_modulo_param_rename(
1371 &lhs_inputs,
1372 &hom("x", "y"),
1373 &rhs_inputs,
1374 &hom("y", "x"),
1375 ));
1376 }
1377
1378 #[test]
1379 fn signature_equivalence_rejects_arity_mismatch() {
1380 use crate::op::Implicit;
1381 let lhs_inputs = vec![(Arc::from("x"), SortExpr::from("Ob"), Implicit::No)];
1382 let rhs_inputs: Vec<(Arc<str>, SortExpr, Implicit)> = Vec::new();
1383 assert!(!signatures_equivalent_modulo_param_rename(
1384 &lhs_inputs,
1385 &SortExpr::from("Ob"),
1386 &rhs_inputs,
1387 &SortExpr::from("Ob"),
1388 ));
1389 }
1390
1391 #[test]
1392 fn sort_params_rename_alpha_equivalent() {
1393 let lhs = vec![SortParam::new("a", "Ob"), SortParam::new("b", "Ob")];
1395 let rhs = vec![SortParam::new("p", "Ob"), SortParam::new("q", "Ob")];
1396 assert!(sort_params_equivalent_modulo_rename(&lhs, &rhs));
1397 }
1398
1399 #[test]
1400 fn sort_params_rename_detects_dependent_difference() {
1401 let lhs = vec![
1403 SortParam::new("Gamma", "Ctx"),
1404 SortParam::new(
1405 "A",
1406 SortExpr::App {
1407 name: Arc::from("Ty"),
1408 args: vec![Term::var("Gamma")],
1409 },
1410 ),
1411 ];
1412 let rhs = vec![
1413 SortParam::new("G", "Ctx"),
1414 SortParam::new(
1415 "A",
1416 SortExpr::App {
1417 name: Arc::from("Ty"),
1418 args: vec![Term::var("G")],
1419 },
1420 ),
1421 ];
1422 assert!(sort_params_equivalent_modulo_rename(&lhs, &rhs));
1423 }
1424
1425 #[test]
1426 fn sort_params_rename_rejects_genuine_difference() {
1427 let lhs = vec![
1431 SortParam::new("Gamma", "Ctx"),
1432 SortParam::new(
1433 "A",
1434 SortExpr::App {
1435 name: Arc::from("Ty"),
1436 args: vec![Term::var("Gamma")],
1437 },
1438 ),
1439 ];
1440 let rhs = vec![
1441 SortParam::new("G", "Ctx"),
1442 SortParam::new(
1443 "A",
1444 SortExpr::App {
1445 name: Arc::from("Ty"),
1446 args: vec![Term::var("A")],
1447 },
1448 ),
1449 ];
1450 assert!(!sort_params_equivalent_modulo_rename(&lhs, &rhs));
1451 }
1452
1453 mod property {
1454 use super::*;
1455 use proptest::prelude::*;
1456
1457 fn arb_name() -> impl Strategy<Value = Arc<str>> {
1458 prop::sample::select(&["S", "T", "Hom", "Tm", "Ob"][..]).prop_map(Arc::from)
1459 }
1460
1461 fn arb_term(depth: usize) -> BoxedStrategy<Term> {
1462 if depth == 0 {
1463 prop::sample::select(&["x", "y", "z"][..])
1464 .prop_map(|s| Term::var(Arc::from(s)))
1465 .boxed()
1466 } else {
1467 let leaf = prop::sample::select(&["x", "y", "z"][..])
1468 .prop_map(|s| Term::var(Arc::from(s)));
1469 let app = (
1470 prop::sample::select(&["f", "g"][..]).prop_map(Arc::from),
1471 prop::collection::vec(arb_term(depth - 1), 0..=2),
1472 )
1473 .prop_map(|(op, args)| Term::App { op, args });
1474 prop_oneof![leaf, app].boxed()
1475 }
1476 }
1477
1478 fn arb_sort_expr() -> BoxedStrategy<SortExpr> {
1479 prop_oneof![
1480 arb_name().prop_map(SortExpr::Name),
1481 (arb_name(), prop::collection::vec(arb_term(1), 0..=3))
1482 .prop_map(|(name, args)| SortExpr::app(name, args))
1483 ]
1484 .boxed()
1485 }
1486
1487 fn arb_subst() -> BoxedStrategy<FxHashMap<Arc<str>, Term>> {
1488 prop::collection::vec(
1489 (
1490 prop::sample::select(&["x", "y", "z"][..]).prop_map(Arc::from),
1491 arb_term(1),
1492 ),
1493 0..=3,
1494 )
1495 .prop_map(|pairs| {
1496 let mut m = FxHashMap::default();
1497 for (k, v) in pairs {
1498 m.insert(k, v);
1499 }
1500 m
1501 })
1502 .boxed()
1503 }
1504
1505 proptest! {
1506 #![proptest_config(ProptestConfig::with_cases(256))]
1507
1508 #[test]
1509 fn subst_empty_is_identity(e in arb_sort_expr()) {
1510 let empty = FxHashMap::default();
1511 prop_assert_eq!(e.subst(&empty), e);
1512 }
1513
1514 #[test]
1515 fn subst_preserves_head(e in arb_sort_expr(), sigma in arb_subst()) {
1516 let after = e.subst(&sigma);
1517 prop_assert_eq!(e.head(), after.head());
1518 }
1519
1520 #[test]
1521 fn normalization_is_idempotent(e in arb_sort_expr()) {
1522 let n1 = e.normalize();
1523 let n2 = n1.clone().normalize();
1524 prop_assert_eq!(n1, n2);
1525 }
1526
1527 #[test]
1528 fn sig_equivalence_is_reflexive(
1529 raw_inputs in prop::collection::vec(
1530 (prop::sample::select(&["x", "y", "z"][..]).prop_map(Arc::from), arb_sort_expr()),
1531 0..=3,
1532 ),
1533 output in arb_sort_expr(),
1534 ) {
1535 let inputs: Vec<(Arc<str>, SortExpr, crate::op::Implicit)> = raw_inputs
1536 .into_iter()
1537 .map(|(n, s)| (n, s, crate::op::Implicit::No))
1538 .collect();
1539 prop_assert!(signatures_equivalent_modulo_param_rename(
1540 &inputs, &output, &inputs, &output,
1541 ));
1542 }
1543
1544 #[test]
1545 fn sig_equivalence_under_alpha_rename(
1546 sort_name in arb_name(),
1547 first in prop::sample::select(&["x", "y", "z"][..]).prop_map(Arc::from),
1548 replacement in prop::sample::select(&["p", "q", "r"][..]).prop_map(Arc::from),
1549 ) {
1550 let lhs_inputs: Vec<(Arc<str>, SortExpr, crate::op::Implicit)> = vec![(
1554 Arc::clone(&first),
1555 SortExpr::Name(Arc::clone(&sort_name)),
1556 crate::op::Implicit::No,
1557 )];
1558 let lhs_output = SortExpr::App {
1559 name: Arc::clone(&sort_name),
1560 args: vec![Term::Var(Arc::clone(&first))],
1561 };
1562 let rhs_inputs: Vec<(Arc<str>, SortExpr, crate::op::Implicit)> = vec![(
1563 Arc::clone(&replacement),
1564 SortExpr::Name(Arc::clone(&sort_name)),
1565 crate::op::Implicit::No,
1566 )];
1567 let rhs_output = SortExpr::App {
1568 name: sort_name,
1569 args: vec![Term::Var(replacement)],
1570 };
1571 prop_assert!(signatures_equivalent_modulo_param_rename(
1572 &lhs_inputs, &lhs_output, &rhs_inputs, &rhs_output,
1573 ));
1574 }
1575
1576 #[test]
1577 fn name_and_empty_app_hash_equal(name in arb_name()) {
1578 use std::collections::hash_map::DefaultHasher;
1579 use std::hash::{Hash, Hasher};
1580 let a = SortExpr::Name(Arc::clone(&name));
1581 let b = SortExpr::App { name, args: Vec::new() };
1582 let mut ha = DefaultHasher::new();
1583 a.hash(&mut ha);
1584 let mut hb = DefaultHasher::new();
1585 b.hash(&mut hb);
1586 prop_assert_eq!(ha.finish(), hb.finish());
1587 prop_assert_eq!(a, b);
1588 }
1589 }
1590 }
1591
1592 #[test]
1593 fn coercion_class_serde_wire_format_is_pascal_case() {
1594 let cases = [
1601 (CoercionClass::Iso, "\"Iso\""),
1602 (CoercionClass::Retraction, "\"Retraction\""),
1603 (CoercionClass::Projection, "\"Projection\""),
1604 (CoercionClass::Opaque, "\"Opaque\""),
1605 ];
1606 for (value, expected) in cases {
1607 match serde_json::to_string(&value) {
1608 Ok(s) => assert_eq!(s, expected, "unexpected wire format"),
1609 Err(e) => panic!("serde failed to serialize a plain enum: {e}"),
1610 }
1611 }
1612 }
1613}