1use ascent::Lattice;
2use ascent::lattice::BoundedLattice;
3use hugr_core::Node;
4use hugr_core::types::{SumType, Type, TypeArg, TypeEnum, TypeRow};
5use itertools::{Itertools, zip_eq};
6use std::cmp::Ordering;
7use std::collections::HashMap;
8use std::hash::{Hash, Hasher};
9use thiserror::Error;
10
11use super::row_contains_bottom;
12
13#[deprecated(
16 note = "`hugr-passes` is deprecated. Use tket::passes instead",
17 since = "0.26.2"
18)]
19pub trait AbstractValue: Clone + std::fmt::Debug + PartialEq + Eq + Hash {
20 fn try_join(self, other: Self) -> Option<(Self, bool)> {
29 (self == other).then_some((self, false))
30 }
31
32 fn try_meet(self, other: Self) -> Option<(Self, bool)> {
40 (self == other).then_some((self, false))
41 }
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
47#[deprecated(
48 note = "`hugr-passes` is deprecated. Use tket::passes instead",
49 since = "0.26.2"
50)]
51pub struct Sum<V> {
52 pub tag: usize,
54 pub values: Vec<V>,
58 pub st: SumType,
60}
61
62#[derive(Clone, Debug, Hash, PartialEq, Eq)]
65#[deprecated(
66 note = "`hugr-passes` is deprecated. Use tket::passes instead",
67 since = "0.26.2"
68)]
69pub struct LoadedFunction<N> {
70 pub func_node: N,
72 pub args: Vec<TypeArg>,
74}
75
76#[derive(PartialEq, Clone, Eq)]
79#[deprecated(
80 note = "`hugr-passes` is deprecated. Use tket::passes instead",
81 since = "0.26.2"
82)]
83pub struct PartialSum<V, N = Node>(pub HashMap<usize, Vec<PartialValue<V, N>>>);
84
85impl<V, N> PartialSum<V, N> {
86 pub fn new_variant(tag: usize, values: impl IntoIterator<Item = PartialValue<V, N>>) -> Self {
89 Self(HashMap::from([(tag, Vec::from_iter(values))]))
90 }
91
92 #[must_use]
95 pub fn num_variants(&self) -> usize {
96 self.0.len()
97 }
98
99 fn assert_invariants(&self) {
100 assert_ne!(self.num_variants(), 0);
101 for pv in self.0.values().flat_map(|x| x.iter()) {
102 pv.assert_invariants();
103 }
104 }
105
106 #[must_use]
108 pub fn supports_tag(&self, tag: usize) -> bool {
109 self.0.contains_key(&tag)
110 }
111
112 #[must_use]
114 pub fn contains_bottom(&self) -> bool {
115 self.0
116 .iter()
117 .all(|(_tag, elements)| row_contains_bottom(elements))
118 }
119}
120
121impl<V: AbstractValue, N: PartialEq + PartialOrd> PartialSum<V, N> {
122 pub fn try_join_mut(&mut self, other: Self) -> Result<bool, usize> {
127 for (k, v) in &other.0 {
128 if self.0.get(k).is_some_and(|row| row.len() != v.len()) {
129 return Err(*k);
130 }
131 }
132 let mut changed = false;
133
134 for (k, v) in other.0 {
135 if let Some(row) = self.0.get_mut(&k) {
136 for (lhs, rhs) in zip_eq(row.iter_mut(), v) {
137 changed |= lhs.join_mut(rhs);
138 }
139 } else {
140 self.0.insert(k, v);
141 changed = true;
142 }
143 }
144 Ok(changed)
145 }
146
147 pub fn try_meet_mut(&mut self, other: Self) -> Result<bool, Option<usize>> {
155 let mut changed = false;
156 let mut keys_to_remove = vec![];
157 for (k, v) in &self.0 {
158 match other.0.get(k) {
159 None => keys_to_remove.push(*k),
160 Some(o_v) => {
161 if v.len() != o_v.len() {
162 return Err(Some(*k));
163 }
164 }
165 }
166 }
167 if keys_to_remove.len() == self.0.len() {
168 return Err(None);
169 }
170 for (k, v) in other.0 {
171 if let Some(row) = self.0.get_mut(&k) {
172 for (lhs, rhs) in zip_eq(row.iter_mut(), v) {
173 changed |= lhs.meet_mut(rhs);
174 }
175 } else {
176 keys_to_remove.push(k);
177 }
178 }
179 for k in keys_to_remove {
180 self.0.remove(&k);
181 changed = true;
182 }
183 Ok(changed)
184 }
185}
186
187#[deprecated(
196 note = "`hugr-passes` is deprecated. Use tket::passes instead",
197 since = "0.26.2"
198)]
199pub trait AsConcrete<V, N>: Sized {
200 type ValErr: std::error::Error;
202 type SumErr: std::error::Error;
205
206 fn from_value(val: V) -> Result<Self, Self::ValErr>;
208
209 fn from_sum(sum: Sum<Self>) -> Result<Self, Self::SumErr>;
211
212 fn from_func(func: LoadedFunction<N>) -> Result<Self, LoadedFunction<N>>;
214}
215
216impl<V: AbstractValue, N: std::fmt::Debug> PartialSum<V, N> {
217 #[expect(clippy::type_complexity)] pub fn try_into_sum<C: AsConcrete<V, N>>(
227 self,
228 typ: &Type,
229 ) -> Result<Sum<C>, ExtractValueError<V, N, C::ValErr, C::SumErr>> {
230 if self.0.len() != 1 {
231 return Err(ExtractValueError::MultipleVariants(self));
232 }
233 let (tag, v) = self.0.into_iter().exactly_one().unwrap();
234 if let TypeEnum::Sum(st) = typ.as_type_enum()
235 && let Some(r) = st.get_variant(tag)
236 && let Ok(r) = TypeRow::try_from(r.clone())
237 && v.len() == r.len()
238 {
239 return Ok(Sum {
240 tag,
241 values: zip_eq(v, r.iter())
242 .map(|(v, t)| v.try_into_concrete(t))
243 .collect::<Result<Vec<_>, _>>()?,
244 st: st.clone(),
245 });
246 }
247 Err(ExtractValueError::BadSumType {
248 typ: typ.clone(),
249 tag,
250 num_elements: v.len(),
251 })
252 }
253}
254
255#[derive(Clone, Debug, PartialEq, Eq, Error)]
258#[deprecated(
259 note = "`hugr-passes` is deprecated. Use tket::passes instead",
260 since = "0.26.2"
261)]
262pub enum ExtractValueError<V, N, VE, SE> {
263 #[error("PartialSum value had multiple possible tags: {0}")]
264 MultipleVariants(PartialSum<V, N>),
265 #[error("Value contained `Bottom`")]
266 ValueIsBottom,
267 #[error("Value contained `Top`")]
268 ValueIsTop,
269 #[error("Could not convert element from abstract value into concrete: {0}")]
270 CouldNotConvert(V, #[source] VE),
271 #[error("Could not build Sum from concrete element values")]
272 CouldNotBuildSum(#[source] SE),
273 #[error("Could not convert into concrete function pointer {0}")]
274 CouldNotLoadFunction(LoadedFunction<N>),
275 #[error("Expected a SumType with tag {tag} having {num_elements} elements, found {typ}")]
276 BadSumType {
277 typ: Type,
278 tag: usize,
279 num_elements: usize,
280 },
281}
282
283impl<V: Clone, N: Clone> PartialSum<V, N> {
284 #[must_use]
286 pub fn variant_values(&self, variant: usize) -> Option<Vec<PartialValue<V, N>>> {
287 self.0.get(&variant).cloned()
288 }
289}
290
291impl<V: PartialEq, N: PartialEq + PartialOrd> PartialOrd for PartialSum<V, N> {
292 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
293 let max_key = self.0.keys().chain(other.0.keys()).copied().max().unwrap();
294 let (mut keys1, mut keys2) = (vec![0; max_key + 1], vec![0; max_key + 1]);
295 for k in self.0.keys() {
296 keys1[*k] = 1;
297 }
298
299 for k in other.0.keys() {
300 keys2[*k] = 1;
301 }
302
303 Some(match keys1.cmp(&keys2) {
304 ord @ (Ordering::Greater | Ordering::Less) => ord,
305 Ordering::Equal => {
306 for (k, lhs) in &self.0 {
307 let Some(rhs) = other.0.get(k) else {
308 unreachable!()
309 };
310 let key_cmp = lhs.partial_cmp(rhs);
311 if key_cmp != Some(Ordering::Equal) {
312 return key_cmp;
313 }
314 }
315 Ordering::Equal
316 }
317 })
318 }
319}
320
321impl<V: std::fmt::Debug, N: std::fmt::Debug> std::fmt::Debug for PartialSum<V, N> {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 self.0.fmt(f)
324 }
325}
326
327impl<V: Hash, N: Hash> Hash for PartialSum<V, N> {
328 fn hash<H: Hasher>(&self, state: &mut H) {
329 for (k, v) in &self.0 {
330 k.hash(state);
331 v.hash(state);
332 }
333 }
334}
335
336#[derive(PartialEq, Clone, Eq, Hash, Debug)]
340#[deprecated(
341 note = "`hugr-passes` is deprecated. Use tket::passes instead",
342 since = "0.26.2"
343)]
344pub enum PartialValue<V, N = Node> {
345 Bottom,
347 LoadedFunction(LoadedFunction<N>),
349 Value(V),
351 PartialSum(PartialSum<V, N>),
353 Top,
355}
356
357impl<V, N> From<V> for PartialValue<V, N> {
358 fn from(v: V) -> Self {
359 Self::Value(v)
360 }
361}
362
363impl<V, N> From<PartialSum<V, N>> for PartialValue<V, N> {
364 fn from(v: PartialSum<V, N>) -> Self {
365 Self::PartialSum(v)
366 }
367}
368
369impl<V, N> PartialValue<V, N> {
370 fn assert_invariants(&self) {
371 if let Self::PartialSum(ps) = self {
372 ps.assert_invariants();
373 }
374 }
375
376 pub fn new_variant(tag: usize, values: impl IntoIterator<Item = Self>) -> Self {
378 PartialSum::new_variant(tag, values).into()
379 }
380
381 #[must_use]
383 pub fn new_unit() -> Self {
384 Self::new_variant(0, [])
385 }
386
387 pub fn new_load(func_node: N, args: impl Into<Vec<TypeArg>>) -> Self {
389 Self::LoadedFunction(LoadedFunction {
390 func_node,
391 args: args.into(),
392 })
393 }
394
395 pub fn supports_tag(&self, tag: usize) -> bool {
397 match self {
398 PartialValue::Bottom | PartialValue::Value(_) | PartialValue::LoadedFunction(_) => {
399 false
400 }
401 PartialValue::PartialSum(ps) => ps.supports_tag(tag),
402 PartialValue::Top => true,
403 }
404 }
405
406 pub fn contains_bottom(&self) -> bool {
411 match self {
412 PartialValue::Bottom => true,
413 PartialValue::Top | PartialValue::Value(_) | PartialValue::LoadedFunction(_) => false,
414 PartialValue::PartialSum(ps) => ps.contains_bottom(),
415 }
416 }
417}
418
419impl<V: AbstractValue, N: Clone> PartialValue<V, N> {
420 pub fn variant_values(&self, tag: usize, len: usize) -> Option<Vec<PartialValue<V, N>>> {
426 let vals = match self {
427 PartialValue::Bottom | PartialValue::Value(_) | PartialValue::LoadedFunction(_) => {
428 return None;
429 }
430 PartialValue::PartialSum(ps) => ps.variant_values(tag)?,
431 PartialValue::Top => vec![PartialValue::Top; len],
432 };
433 assert_eq!(vals.len(), len);
434 Some(vals)
435 }
436}
437
438impl<V: AbstractValue, N: std::fmt::Debug> PartialValue<V, N> {
439 pub fn try_into_concrete<C: AsConcrete<V, N>>(
449 self,
450 typ: &Type,
451 ) -> Result<C, ExtractValueError<V, N, C::ValErr, C::SumErr>> {
452 match self {
453 Self::Value(v) => {
454 C::from_value(v.clone()).map_err(|e| ExtractValueError::CouldNotConvert(v, e))
455 }
456 Self::LoadedFunction(lf) => {
457 C::from_func(lf).map_err(ExtractValueError::CouldNotLoadFunction)
458 }
459 Self::PartialSum(ps) => {
460 C::from_sum(ps.try_into_sum(typ)?).map_err(ExtractValueError::CouldNotBuildSum)
461 }
462 Self::Top => Err(ExtractValueError::ValueIsTop),
463 Self::Bottom => Err(ExtractValueError::ValueIsBottom),
464 }
465 }
466}
467
468impl<V: AbstractValue, N: PartialEq + PartialOrd> Lattice for PartialValue<V, N> {
469 fn join_mut(&mut self, other: Self) -> bool {
470 self.assert_invariants();
471 let mut old_self = Self::Top;
472 std::mem::swap(self, &mut old_self);
473 let (res, ch) = match (old_self, other) {
474 (old @ Self::Top, _) | (old, Self::Bottom) => (old, false),
475 (_, other @ Self::Top) | (Self::Bottom, other) => (other, true),
476 (Self::Value(h1), Self::Value(h2)) => match h1.clone().try_join(h2) {
477 Some((h3, b)) => (Self::Value(h3), b),
478 None => (Self::Top, true),
479 },
480 (Self::LoadedFunction(lf1), Self::LoadedFunction(lf2))
481 if lf1.func_node == lf2.func_node =>
482 {
483 (Self::LoadedFunction(lf1), false)
485 }
486 (Self::PartialSum(mut ps1), Self::PartialSum(ps2)) => match ps1.try_join_mut(ps2) {
487 Ok(ch) => (Self::PartialSum(ps1), ch),
488 Err(_) => (Self::Top, true),
489 },
490 _ => (Self::Top, true),
491 };
492 *self = res;
493 ch
494 }
495
496 fn meet_mut(&mut self, other: Self) -> bool {
497 self.assert_invariants();
498 let mut old_self = Self::Bottom;
499 std::mem::swap(self, &mut old_self);
500 let (res, ch) = match (old_self, other) {
501 (old @ Self::Bottom, _) | (old, Self::Top) => (old, false),
502 (_, other @ Self::Bottom) | (Self::Top, other) => (other, true),
503 (Self::Value(h1), Self::Value(h2)) => match h1.try_meet(h2) {
504 Some((h3, ch)) => (Self::Value(h3), ch),
505 None => (Self::Bottom, true),
506 },
507 (Self::LoadedFunction(lf1), Self::LoadedFunction(lf2))
508 if lf1.func_node == lf2.func_node =>
509 {
510 (Self::LoadedFunction(lf1), false)
512 }
513 (Self::PartialSum(mut ps1), Self::PartialSum(ps2)) => match ps1.try_meet_mut(ps2) {
514 Ok(ch) => (Self::PartialSum(ps1), ch),
515 Err(_) => (Self::Bottom, true),
516 },
517 _ => (Self::Bottom, true),
518 };
519 *self = res;
520 ch
521 }
522}
523
524impl<V: AbstractValue, N: PartialEq + PartialOrd> BoundedLattice for PartialValue<V, N> {
525 fn top() -> Self {
526 Self::Top
527 }
528
529 fn bottom() -> Self {
530 Self::Bottom
531 }
532}
533
534impl<V: PartialEq, N: PartialEq + PartialOrd> PartialOrd for PartialValue<V, N> {
535 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
536 use std::cmp::Ordering;
537 match (self, other) {
538 (Self::Bottom, Self::Bottom) => Some(Ordering::Equal),
539 (Self::Top, Self::Top) => Some(Ordering::Equal),
540 (Self::Bottom, _) => Some(Ordering::Less),
541 (_, Self::Bottom) => Some(Ordering::Greater),
542 (Self::Top, _) => Some(Ordering::Greater),
543 (_, Self::Top) => Some(Ordering::Less),
544 (Self::Value(v1), Self::Value(v2)) => (v1 == v2).then_some(Ordering::Equal),
545 (Self::LoadedFunction(lf1), Self::LoadedFunction(lf2)) => {
546 (lf1 == lf2).then_some(Ordering::Equal)
547 }
548 (Self::PartialSum(ps1), Self::PartialSum(ps2)) => ps1.partial_cmp(ps2),
549 _ => None,
550 }
551 }
552}
553
554#[cfg(test)]
555mod test {
556 use std::sync::Arc;
557
558 use ascent::{Lattice, lattice::BoundedLattice};
559 use hugr_core::NodeIndex;
560 use itertools::{Itertools as _, zip_eq};
561 use prop::sample::subsequence;
562 use proptest::prelude::*;
563
564 use proptest_recurse::{StrategyExt, StrategySet};
565
566 use super::{AbstractValue, LoadedFunction, PartialSum, PartialValue};
567
568 #[derive(Debug, PartialEq, Eq, Clone)]
569 enum TestSumType {
570 Branch(Vec<Vec<Arc<TestSumType>>>),
571 LeafVal(usize), LeafPtr(usize), }
574
575 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
576 struct TestValue(usize);
577
578 impl AbstractValue for TestValue {}
579
580 #[derive(Clone)]
581 struct SumTypeParams {
582 depth: usize,
583 desired_size: usize,
584 expected_branch_size: usize,
585 }
586
587 impl Default for SumTypeParams {
588 fn default() -> Self {
589 Self {
590 depth: 5,
591 desired_size: 20,
592 expected_branch_size: 5,
593 }
594 }
595 }
596
597 impl TestSumType {
598 fn check_value(&self, pv: &PartialValue<TestValue>) -> bool {
599 match (self, pv) {
600 (_, PartialValue::Bottom | PartialValue::Top) => true,
601 (Self::LeafVal(max), PartialValue::Value(TestValue(val))) => val <= max,
602 (
603 Self::LeafPtr(max),
604 PartialValue::LoadedFunction(LoadedFunction { func_node, args }),
605 ) => args.is_empty() && func_node.index() <= *max,
606 (Self::Branch(sop), PartialValue::PartialSum(ps)) => {
607 for (k, v) in &ps.0 {
608 if *k >= sop.len() {
609 return false;
610 }
611 let prod = &sop[*k];
612 if prod.len() != v.len() {
613 return false;
614 }
615 if !zip_eq(prod, v).all(|(lhs, rhs)| lhs.check_value(rhs)) {
616 return false;
617 }
618 }
619 true
620 }
621 _ => false,
622 }
623 }
624 }
625
626 impl Arbitrary for TestSumType {
627 type Parameters = SumTypeParams;
628 type Strategy = SBoxedStrategy<Self>;
629 fn arbitrary_with(params: Self::Parameters) -> Self::Strategy {
630 fn arb(params: SumTypeParams, set: &mut StrategySet) -> SBoxedStrategy<TestSumType> {
631 use proptest::collection::vec;
632 let leaf_strat = prop_oneof![
633 (0..usize::MAX).prop_map(TestSumType::LeafVal),
634 (0..((2usize ^ 31) - 2)).prop_map(TestSumType::LeafPtr)
636 ];
637 leaf_strat.prop_mutually_recursive(
638 params.depth as u32,
639 params.desired_size as u32,
640 params.expected_branch_size as u32,
641 set,
642 move |set| {
643 let params2 = params.clone();
644 vec(
645 vec(
646 set.get::<TestSumType, _>(move |set| arb(params2, set))
647 .prop_map(Arc::new),
648 1..=params.expected_branch_size,
649 ),
650 1..=params.expected_branch_size,
651 )
652 .prop_map(TestSumType::Branch)
653 .sboxed()
654 },
655 )
656 }
657
658 arb(params, &mut StrategySet::default())
659 }
660 }
661
662 fn single_sum_strat(
663 tag: usize,
664 elems: Vec<Arc<TestSumType>>,
665 ) -> impl Strategy<Value = PartialSum<TestValue>> {
666 elems
667 .iter()
668 .map(Arc::as_ref)
669 .map(any_partial_value_of_type)
670 .collect::<Vec<_>>()
671 .prop_map(move |elems| PartialSum::new_variant(tag, elems))
672 }
673
674 fn partial_sum_strat(
675 variants: &[Vec<Arc<TestSumType>>],
676 ) -> impl Strategy<Value = PartialSum<TestValue>> + use<> {
677 let tagged_variants = variants.iter().cloned().enumerate().collect::<Vec<_>>();
679 let sum_variants_strat: BoxedStrategy<Vec<PartialSum<TestValue>>> =
681 subsequence(tagged_variants, 1..=variants.len())
682 .prop_flat_map(|selected_variants| {
683 selected_variants
684 .into_iter()
685 .map(|(tag, elems)| single_sum_strat(tag, elems))
686 .collect::<Vec<_>>()
687 })
688 .boxed();
689 sum_variants_strat.prop_map(|psums: Vec<PartialSum<TestValue>>| {
690 let mut psums = psums.into_iter();
691 let first = psums.next().unwrap();
692 psums.fold(first, |mut a, b| {
693 a.try_join_mut(b).unwrap();
694 a
695 })
696 })
697 }
698
699 fn any_partial_value_of_type(
700 ust: &TestSumType,
701 ) -> impl Strategy<Value = PartialValue<TestValue>> + use<> {
702 match ust {
703 TestSumType::LeafVal(i) => (0..=*i)
704 .prop_map(TestValue)
705 .prop_map(PartialValue::from)
706 .boxed(),
707 TestSumType::LeafPtr(i) => (0..=*i)
708 .prop_map(|i| {
709 PartialValue::LoadedFunction(LoadedFunction {
710 func_node: portgraph::NodeIndex::new(i).into(),
711 args: vec![],
712 })
713 })
714 .boxed(),
715 TestSumType::Branch(sop) => partial_sum_strat(sop).prop_map(PartialValue::from).boxed(),
716 }
717 }
718
719 fn any_partial_value_with(
720 params: <TestSumType as Arbitrary>::Parameters,
721 ) -> impl Strategy<Value = PartialValue<TestValue>> {
722 any_with::<TestSumType>(params).prop_flat_map(|t| any_partial_value_of_type(&t))
723 }
724
725 fn any_partial_value() -> impl Strategy<Value = PartialValue<TestValue>> {
726 any_partial_value_with(Default::default())
727 }
728
729 fn any_partial_values<const N: usize>() -> impl Strategy<Value = [PartialValue<TestValue>; N]> {
730 any::<TestSumType>().prop_flat_map(|ust| {
731 TryInto::<[_; N]>::try_into(
732 (0..N)
733 .map(|_| any_partial_value_of_type(&ust))
734 .collect_vec(),
735 )
736 .unwrap()
737 })
738 }
739
740 fn any_typed_partial_value() -> impl Strategy<Value = (TestSumType, PartialValue<TestValue>)> {
741 any::<TestSumType>()
742 .prop_flat_map(|t| any_partial_value_of_type(&t).prop_map(move |v| (t.clone(), v)))
743 }
744
745 proptest! {
746 #[test]
747 fn partial_value_type((tst, pv) in any_typed_partial_value()) {
748 prop_assert!(tst.check_value(&pv));
749 }
750
751 #[test]
757 fn partial_value_valid(pv in any_partial_value()) {
758 pv.assert_invariants();
759 }
760
761 #[test]
762 fn bounded_lattice(v in any_partial_value()) {
763 prop_assert!(v <= PartialValue::top());
764 prop_assert!(v >= PartialValue::bottom());
765 }
766
767 #[test]
768 fn meet_join_self_noop(v1 in any_partial_value()) {
769 let mut subject = v1.clone();
770
771 assert_eq!(v1.clone(), v1.clone().join(v1.clone()));
772 assert!(!subject.join_mut(v1.clone()));
773 assert_eq!(subject, v1);
774
775 assert_eq!(v1.clone(), v1.clone().meet(v1.clone()));
776 assert!(!subject.meet_mut(v1.clone()));
777 assert_eq!(subject, v1);
778 }
779
780 #[test]
781 fn lattice([v1,v2] in any_partial_values()) {
782 let meet = v1.clone().meet(v2.clone());
783 prop_assert!(meet <= v1, "meet not less <=: {:#?}", &meet);
784 prop_assert!(meet <= v2, "meet not less <=: {:#?}", &meet);
785 prop_assert!(meet == v2.clone().meet(v1.clone()), "meet not symmetric");
786 prop_assert!(meet == meet.clone().meet(v1.clone()), "repeated meet should be a no-op");
787 prop_assert!(meet == meet.clone().meet(v2.clone()), "repeated meet should be a no-op");
788
789 let join = v1.clone().join(v2.clone());
790 prop_assert!(join >= v1, "join not >=: {:#?}", &join);
791 prop_assert!(join >= v2, "join not >=: {:#?}", &join);
792 prop_assert!(join == v2.clone().join(v1.clone()), "join not symmetric");
793 prop_assert!(join == join.clone().join(v1.clone()), "repeated join should be a no-op");
794 prop_assert!(join == join.clone().join(v2.clone()), "repeated join should be a no-op");
795 }
796
797 #[test]
798 fn lattice_associative([v1, v2, v3] in any_partial_values()) {
799 let a = v1.clone().meet(v2.clone()).meet(v3.clone());
800 let b = v1.clone().meet(v2.clone().meet(v3.clone()));
801 prop_assert!(a==b, "meet not associative");
802
803 let a = v1.clone().join(v2.clone()).join(v3.clone());
804 let b = v1.clone().join(v2.clone().join(v3.clone()));
805 prop_assert!(a==b, "join not associative");
806 }
807 }
808}