Skip to main content

ftui_core/
generic_diff.rs

1//! Structural diff and patch via datatype-generic representation.
2//!
3//! Works with any type implementing [`GenericRepr`] by computing diffs on
4//! the Sum/Product/Unit encoding and applying patches to transform values.
5//!
6//! # Example
7//!
8//! ```
9//! use ftui_core::generic_repr::*;
10//! use ftui_core::generic_diff::*;
11//!
12//! #[derive(Clone, Debug, PartialEq)]
13//! struct Point { x: f64, y: f64 }
14//!
15//! impl GenericRepr for Point {
16//!     type Repr = Product<f64, Product<f64, Unit>>;
17//!     fn into_repr(self) -> Self::Repr {
18//!         Product(self.x, Product(self.y, Unit))
19//!     }
20//!     fn from_repr(repr: Self::Repr) -> Self {
21//!         Point { x: repr.0, y: repr.1.0 }
22//!     }
23//! }
24//!
25//! let old = Point { x: 1.0, y: 2.0 };
26//! let new = Point { x: 1.0, y: 3.0 };
27//! let diff = generic_diff(&old, &new);
28//! let patched = generic_patch(&old, &diff);
29//! assert_eq!(patched, new);
30//! assert!(!diff.is_empty());
31//! ```
32
33use crate::generic_repr::*;
34use std::fmt;
35
36// ── Delta type ──────────────────────────────────────────────────────
37
38/// A change delta: either unchanged or replaced with a new value.
39#[derive(Clone, PartialEq, Eq)]
40pub enum Delta<T> {
41    /// Value is unchanged.
42    Same,
43    /// Value was replaced.
44    Changed(T),
45}
46
47impl<T: fmt::Debug> fmt::Debug for Delta<T> {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Delta::Same => write!(f, "Same"),
51            Delta::Changed(v) => write!(f, "Changed({v:?})"),
52        }
53    }
54}
55
56impl<T> Delta<T> {
57    /// Whether this delta represents no change.
58    pub fn is_same(&self) -> bool {
59        matches!(self, Delta::Same)
60    }
61
62    /// Whether this delta represents a change.
63    pub fn is_changed(&self) -> bool {
64        matches!(self, Delta::Changed(_))
65    }
66}
67
68// ── Diff trait ──────────────────────────────────────────────────────
69
70/// Compute a structural diff between two values of the same type.
71///
72/// The diff type `Self::Diff` captures what changed between `old` and `new`.
73pub trait Diff: Sized {
74    /// The diff representation for this type.
75    type Diff: DiffInfo;
76
77    /// Compute the diff from `old` to `new`.
78    fn diff(old: &Self, new: &Self) -> Self::Diff;
79}
80
81/// Metadata about a diff.
82pub trait DiffInfo {
83    /// Whether the diff represents no changes (identity patch).
84    fn is_empty(&self) -> bool;
85    /// Number of changed fields/variants.
86    fn change_count(&self) -> usize;
87}
88
89// ── Patch trait ─────────────────────────────────────────────────────
90
91/// Apply a diff to transform a value.
92pub trait Patch: Diff {
93    /// Apply the diff to `old` to produce `new`.
94    fn patch(old: &Self, diff: &Self::Diff) -> Self;
95}
96
97// ── Leaf impls ──────────────────────────────────────────────────────
98
99/// Blanket leaf-level diff: compare via PartialEq, delta is the new value.
100impl<T: Clone + PartialEq> Diff for T
101where
102    T: LeafDiff,
103{
104    type Diff = Delta<T>;
105
106    fn diff(old: &Self, new: &Self) -> Delta<T> {
107        if old == new {
108            Delta::Same
109        } else {
110            Delta::Changed(new.clone())
111        }
112    }
113}
114
115impl<T: Clone + PartialEq> Patch for T
116where
117    T: LeafDiff,
118{
119    fn patch(old: &Self, diff: &Delta<T>) -> Self {
120        match diff {
121            Delta::Same => old.clone(),
122            Delta::Changed(v) => v.clone(),
123        }
124    }
125}
126
127/// Marker trait for types that use leaf-level (PartialEq) diffing.
128///
129/// Implement this for primitive types and small value types that should
130/// be compared atomically rather than structurally.
131pub trait LeafDiff {}
132
133// Implement LeafDiff for common primitives
134impl LeafDiff for bool {}
135impl LeafDiff for u8 {}
136impl LeafDiff for u16 {}
137impl LeafDiff for u32 {}
138impl LeafDiff for u64 {}
139impl LeafDiff for u128 {}
140impl LeafDiff for usize {}
141impl LeafDiff for i8 {}
142impl LeafDiff for i16 {}
143impl LeafDiff for i32 {}
144impl LeafDiff for i64 {}
145impl LeafDiff for i128 {}
146impl LeafDiff for isize {}
147impl LeafDiff for f32 {}
148impl LeafDiff for f64 {}
149impl LeafDiff for char {}
150impl LeafDiff for String {}
151impl LeafDiff for &str {}
152
153impl<T> DiffInfo for Delta<T> {
154    fn is_empty(&self) -> bool {
155        self.is_same()
156    }
157    fn change_count(&self) -> usize {
158        if self.is_changed() { 1 } else { 0 }
159    }
160}
161
162// ── Product diff ────────────────────────────────────────────────────
163
164/// Diff of a product: diff each component independently.
165#[derive(Clone, Debug, PartialEq, Eq)]
166pub struct ProductDiff<HD, TD> {
167    pub head: HD,
168    pub tail: TD,
169}
170
171impl<HD: DiffInfo, TD: DiffInfo> DiffInfo for ProductDiff<HD, TD> {
172    fn is_empty(&self) -> bool {
173        self.head.is_empty() && self.tail.is_empty()
174    }
175    fn change_count(&self) -> usize {
176        self.head.change_count() + self.tail.change_count()
177    }
178}
179
180impl<H: Diff, T: Diff> Diff for Product<H, T> {
181    type Diff = ProductDiff<H::Diff, T::Diff>;
182
183    fn diff(old: &Self, new: &Self) -> Self::Diff {
184        ProductDiff {
185            head: H::diff(&old.0, &new.0),
186            tail: T::diff(&old.1, &new.1),
187        }
188    }
189}
190
191impl<H: Patch, T: Patch> Patch for Product<H, T> {
192    fn patch(old: &Self, diff: &Self::Diff) -> Self {
193        Product(H::patch(&old.0, &diff.head), T::patch(&old.1, &diff.tail))
194    }
195}
196
197// ── Unit diff ───────────────────────────────────────────────────────
198
199/// Diff of a Unit: always empty (nothing to compare).
200#[derive(Clone, Debug, PartialEq, Eq)]
201pub struct UnitDiff;
202
203impl DiffInfo for UnitDiff {
204    fn is_empty(&self) -> bool {
205        true
206    }
207    fn change_count(&self) -> usize {
208        0
209    }
210}
211
212impl Diff for Unit {
213    type Diff = UnitDiff;
214    fn diff(_old: &Self, _new: &Self) -> UnitDiff {
215        UnitDiff
216    }
217}
218
219impl Patch for Unit {
220    fn patch(_old: &Self, _diff: &UnitDiff) -> Self {
221        Unit
222    }
223}
224
225// ── Sum diff ────────────────────────────────────────────────────────
226
227/// Diff of a sum type: either both are the same variant (structural diff)
228/// or the variant changed (full replacement).
229#[derive(Clone, Debug, PartialEq, Eq)]
230pub enum SumDiff<LD, RD, L, R> {
231    /// Both values are Left; contains diff of inner values.
232    BothLeft(LD),
233    /// Both values are Right; contains diff of inner values.
234    BothRight(RD),
235    /// Variant changed from Left to Right.
236    LeftToRight(R),
237    /// Variant changed from Right to Left.
238    RightToLeft(L),
239}
240
241impl<LD: DiffInfo, RD: DiffInfo, L, R> DiffInfo for SumDiff<LD, RD, L, R> {
242    fn is_empty(&self) -> bool {
243        match self {
244            SumDiff::BothLeft(d) => d.is_empty(),
245            SumDiff::BothRight(d) => d.is_empty(),
246            SumDiff::LeftToRight(_) | SumDiff::RightToLeft(_) => false,
247        }
248    }
249    fn change_count(&self) -> usize {
250        match self {
251            SumDiff::BothLeft(d) => d.change_count(),
252            SumDiff::BothRight(d) => d.change_count(),
253            SumDiff::LeftToRight(_) | SumDiff::RightToLeft(_) => 1,
254        }
255    }
256}
257
258impl<L: Diff + Clone, R: Diff + Clone> Diff for Sum<L, R> {
259    type Diff = SumDiff<L::Diff, R::Diff, L, R>;
260
261    fn diff(old: &Self, new: &Self) -> Self::Diff {
262        match (old, new) {
263            (Sum::Left(o), Sum::Left(n)) => SumDiff::BothLeft(L::diff(o, n)),
264            (Sum::Right(o), Sum::Right(n)) => SumDiff::BothRight(R::diff(o, n)),
265            (Sum::Left(_), Sum::Right(n)) => SumDiff::LeftToRight(n.clone()),
266            (Sum::Right(_), Sum::Left(n)) => SumDiff::RightToLeft(n.clone()),
267        }
268    }
269}
270
271impl<L: Patch + Clone, R: Patch + Clone> Patch for Sum<L, R> {
272    fn patch(old: &Self, diff: &Self::Diff) -> Self {
273        match diff {
274            SumDiff::BothLeft(d) => match old {
275                Sum::Left(o) => Sum::Left(L::patch(o, d)),
276                Sum::Right(_) => unreachable!("BothLeft diff applied to Right"),
277            },
278            SumDiff::BothRight(d) => match old {
279                Sum::Right(o) => Sum::Right(R::patch(o, d)),
280                Sum::Left(_) => unreachable!("BothRight diff applied to Left"),
281            },
282            SumDiff::LeftToRight(v) => Sum::Right(v.clone()),
283            SumDiff::RightToLeft(v) => Sum::Left(v.clone()),
284        }
285    }
286}
287
288// ── Field diff ──────────────────────────────────────────────────────
289
290impl<T: Diff> Diff for Field<T> {
291    type Diff = T::Diff;
292
293    fn diff(old: &Self, new: &Self) -> T::Diff {
294        T::diff(&old.value, &new.value)
295    }
296}
297
298impl<T: Patch> Patch for Field<T> {
299    fn patch(old: &Self, diff: &T::Diff) -> Self {
300        Field {
301            name: old.name,
302            value: T::patch(&old.value, diff),
303        }
304    }
305}
306
307// ── Variant diff ────────────────────────────────────────────────────
308
309impl<T: Diff> Diff for Variant<T> {
310    type Diff = T::Diff;
311
312    fn diff(old: &Self, new: &Self) -> T::Diff {
313        T::diff(&old.value, &new.value)
314    }
315}
316
317impl<T: Patch> Patch for Variant<T> {
318    fn patch(old: &Self, diff: &T::Diff) -> Self {
319        Variant {
320            name: old.name,
321            value: T::patch(&old.value, diff),
322        }
323    }
324}
325
326// ── Void diff ───────────────────────────────────────────────────────
327
328/// Void has no values, so Diff is trivially never called.
329/// We need the impl for sum chains to terminate.
330#[derive(Clone, Debug, PartialEq, Eq)]
331pub enum VoidDiff {}
332
333impl DiffInfo for VoidDiff {
334    fn is_empty(&self) -> bool {
335        match *self {}
336    }
337    fn change_count(&self) -> usize {
338        match *self {}
339    }
340}
341
342impl Diff for Void {
343    type Diff = VoidDiff;
344    fn diff(old: &Self, _new: &Self) -> VoidDiff {
345        match *old {}
346    }
347}
348
349impl Patch for Void {
350    fn patch(old: &Self, _diff: &VoidDiff) -> Self {
351        match *old {}
352    }
353}
354
355// ── Generic diff/patch convenience functions ────────────────────────
356
357/// Compute a structural diff between two values via their generic representation.
358///
359/// Returns the diff of the representation type, which can be used with
360/// [`generic_patch`] to transform `old` into `new`.
361pub fn generic_diff<T>(old: &T, new: &T) -> <T::Repr as Diff>::Diff
362where
363    T: GenericRepr + Clone,
364    T::Repr: Diff,
365{
366    let old_repr = old.clone().into_repr();
367    let new_repr = new.clone().into_repr();
368    T::Repr::diff(&old_repr, &new_repr)
369}
370
371/// Apply a structural diff to transform a value via its generic representation.
372pub fn generic_patch<T>(old: &T, diff: &<T::Repr as Diff>::Diff) -> T
373where
374    T: GenericRepr + Clone,
375    T::Repr: Patch,
376{
377    let old_repr = old.clone().into_repr();
378    let patched_repr = T::Repr::patch(&old_repr, diff);
379    T::from_repr(patched_repr)
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    // ── Test types ────────────────────────────────────────────────
387
388    #[derive(Clone, Debug, PartialEq)]
389    struct Point {
390        x: f64,
391        y: f64,
392    }
393
394    impl GenericRepr for Point {
395        type Repr = Product<Field<f64>, Product<Field<f64>, Unit>>;
396        fn into_repr(self) -> Self::Repr {
397            Product(
398                Field::new("x", self.x),
399                Product(Field::new("y", self.y), Unit),
400            )
401        }
402        fn from_repr(repr: Self::Repr) -> Self {
403            Point {
404                x: repr.0.value,
405                y: repr.1.0.value,
406            }
407        }
408    }
409
410    #[derive(Clone, Debug, PartialEq)]
411    enum Color {
412        Red,
413        Green,
414        Custom(u8, u8, u8),
415    }
416
417    impl GenericRepr for Color {
418        type Repr = Sum<
419            Variant<Unit>,
420            Sum<Variant<Unit>, Sum<Variant<Product<u8, Product<u8, Product<u8, Unit>>>>, Void>>,
421        >;
422
423        fn into_repr(self) -> Self::Repr {
424            match self {
425                Color::Red => Sum::Left(Variant::new("Red", Unit)),
426                Color::Green => Sum::Right(Sum::Left(Variant::new("Green", Unit))),
427                Color::Custom(r, g, b) => Sum::Right(Sum::Right(Sum::Left(Variant::new(
428                    "Custom",
429                    Product(r, Product(g, Product(b, Unit))),
430                )))),
431            }
432        }
433
434        fn from_repr(repr: Self::Repr) -> Self {
435            match repr {
436                Sum::Left(_) => Color::Red,
437                Sum::Right(Sum::Left(_)) => Color::Green,
438                Sum::Right(Sum::Right(Sum::Left(v))) => {
439                    Color::Custom(v.value.0, v.value.1.0, v.value.1.1.0)
440                }
441                Sum::Right(Sum::Right(Sum::Right(v))) => match v {},
442            }
443        }
444    }
445
446    // ── Leaf diff ─────────────────────────────────────────────────
447
448    #[test]
449    fn leaf_diff_same() {
450        let d = i32::diff(&42, &42);
451        assert!(d.is_same());
452        assert!(d.is_empty());
453        assert_eq!(d.change_count(), 0);
454    }
455
456    #[test]
457    fn leaf_diff_changed() {
458        let d = i32::diff(&1, &2);
459        assert!(d.is_changed());
460        assert!(!d.is_empty());
461        assert_eq!(d.change_count(), 1);
462    }
463
464    #[test]
465    fn leaf_patch_same() {
466        let d = Delta::Same;
467        assert_eq!(i32::patch(&42, &d), 42);
468    }
469
470    #[test]
471    fn leaf_patch_changed() {
472        let d = Delta::Changed(99);
473        assert_eq!(i32::patch(&42, &d), 99);
474    }
475
476    // ── Product diff ──────────────────────────────────────────────
477
478    #[test]
479    fn product_diff_same() {
480        let a = Product(1u32, Product(2u32, Unit));
481        let d = <Product<u32, Product<u32, Unit>>>::diff(&a, &a);
482        assert!(d.is_empty());
483        assert_eq!(d.change_count(), 0);
484    }
485
486    #[test]
487    fn product_diff_one_field() {
488        let a = Product(1u32, Product(2u32, Unit));
489        let b = Product(1u32, Product(3u32, Unit));
490        let d = <Product<u32, Product<u32, Unit>>>::diff(&a, &b);
491        assert!(!d.is_empty());
492        assert_eq!(d.change_count(), 1);
493        assert!(d.head.is_same());
494        assert!(d.tail.head.is_changed());
495    }
496
497    #[test]
498    fn product_patch_roundtrip() {
499        let a = Product(1u32, Product(2u32, Unit));
500        let b = Product(3u32, Product(2u32, Unit));
501        let d = <Product<u32, Product<u32, Unit>>>::diff(&a, &b);
502        let patched = <Product<u32, Product<u32, Unit>>>::patch(&a, &d);
503        assert_eq!(patched, b);
504    }
505
506    // ── Sum diff ──────────────────────────────────────────────────
507
508    #[test]
509    fn sum_diff_same_variant() {
510        let a: Sum<u32, u32> = Sum::Left(42);
511        let b: Sum<u32, u32> = Sum::Left(42);
512        let d = <Sum<u32, u32>>::diff(&a, &b);
513        assert!(d.is_empty());
514    }
515
516    #[test]
517    fn sum_diff_same_variant_different_value() {
518        let a: Sum<u32, u32> = Sum::Left(1);
519        let b: Sum<u32, u32> = Sum::Left(2);
520        let d = <Sum<u32, u32>>::diff(&a, &b);
521        assert!(!d.is_empty());
522        assert_eq!(d.change_count(), 1);
523    }
524
525    #[test]
526    fn sum_diff_variant_changed() {
527        let a: Sum<u32, u32> = Sum::Left(1);
528        let b: Sum<u32, u32> = Sum::Right(2);
529        let d = <Sum<u32, u32>>::diff(&a, &b);
530        assert!(!d.is_empty());
531        assert_eq!(d.change_count(), 1);
532    }
533
534    #[test]
535    fn sum_patch_same_variant() {
536        let a: Sum<u32, u32> = Sum::Left(1);
537        let b: Sum<u32, u32> = Sum::Left(2);
538        let d = <Sum<u32, u32>>::diff(&a, &b);
539        let patched = <Sum<u32, u32>>::patch(&a, &d);
540        assert_eq!(patched, b);
541    }
542
543    #[test]
544    fn sum_patch_variant_change() {
545        let a: Sum<u32, u32> = Sum::Left(1);
546        let b: Sum<u32, u32> = Sum::Right(99);
547        let d = <Sum<u32, u32>>::diff(&a, &b);
548        let patched = <Sum<u32, u32>>::patch(&a, &d);
549        assert_eq!(patched, b);
550    }
551
552    // ── Generic diff/patch on Point ───────────────────────────────
553
554    #[test]
555    fn point_diff_same() {
556        let p = Point { x: 1.0, y: 2.0 };
557        let d = generic_diff(&p, &p);
558        assert!(d.is_empty());
559    }
560
561    #[test]
562    fn point_diff_one_field_changed() {
563        let a = Point { x: 1.0, y: 2.0 };
564        let b = Point { x: 1.0, y: 3.0 };
565        let d = generic_diff(&a, &b);
566        assert!(!d.is_empty());
567        assert_eq!(d.change_count(), 1);
568    }
569
570    #[test]
571    fn point_diff_both_fields_changed() {
572        let a = Point { x: 1.0, y: 2.0 };
573        let b = Point { x: 3.0, y: 4.0 };
574        let d = generic_diff(&a, &b);
575        assert_eq!(d.change_count(), 2);
576    }
577
578    #[test]
579    fn point_patch_roundtrip() {
580        let a = Point { x: 1.0, y: 2.0 };
581        let b = Point { x: 3.0, y: 4.0 };
582        let d = generic_diff(&a, &b);
583        let patched = generic_patch(&a, &d);
584        assert_eq!(patched, b);
585    }
586
587    #[test]
588    fn point_patch_identity() {
589        let p = Point { x: 1.0, y: 2.0 };
590        let d = generic_diff(&p, &p);
591        let patched = generic_patch(&p, &d);
592        assert_eq!(patched, p);
593    }
594
595    // ── Generic diff/patch on Color enum ──────────────────────────
596
597    #[test]
598    fn color_diff_same_unit_variant() {
599        let d = generic_diff(&Color::Red, &Color::Red);
600        assert!(d.is_empty());
601    }
602
603    #[test]
604    fn color_diff_different_unit_variants() {
605        let d = generic_diff(&Color::Red, &Color::Green);
606        assert!(!d.is_empty());
607    }
608
609    #[test]
610    fn color_diff_same_data_variant() {
611        let a = Color::Custom(255, 128, 0);
612        let d = generic_diff(&a, &a);
613        assert!(d.is_empty());
614    }
615
616    #[test]
617    fn color_diff_data_variant_changed() {
618        let a = Color::Custom(255, 128, 0);
619        let b = Color::Custom(255, 0, 0);
620        let d = generic_diff(&a, &b);
621        assert!(!d.is_empty());
622    }
623
624    #[test]
625    fn color_patch_variant_change() {
626        let a = Color::Red;
627        let b = Color::Custom(1, 2, 3);
628        let d = generic_diff(&a, &b);
629        let patched = generic_patch(&a, &d);
630        assert_eq!(patched, b);
631    }
632
633    #[test]
634    fn color_patch_roundtrip_all() {
635        let variants = [Color::Red, Color::Green, Color::Custom(10, 20, 30)];
636        for old in &variants {
637            for new in &variants {
638                let d = generic_diff(old, new);
639                let patched = generic_patch(old, &d);
640                assert_eq!(&patched, new, "failed: {old:?} -> {new:?}");
641            }
642        }
643    }
644
645    // ── Delta debug ───────────────────────────────────────────────
646
647    #[test]
648    fn delta_debug_same() {
649        let d: Delta<i32> = Delta::Same;
650        assert_eq!(format!("{d:?}"), "Same");
651    }
652
653    #[test]
654    fn delta_debug_changed() {
655        let d = Delta::Changed(42);
656        assert_eq!(format!("{d:?}"), "Changed(42)");
657    }
658
659    // ── DiffInfo on product ───────────────────────────────────────
660
661    #[test]
662    fn product_diff_change_count() {
663        let a = Product(1u32, Product(2u32, Product(3u32, Unit)));
664        let b = Product(1u32, Product(9u32, Product(9u32, Unit)));
665        let d = <Product<u32, Product<u32, Product<u32, Unit>>>>::diff(&a, &b);
666        assert_eq!(d.change_count(), 2);
667    }
668
669    // ── Proptest property tests ──────────────────────────────────
670
671    mod proptests {
672        use super::*;
673        use proptest::prelude::*;
674
675        fn arb_point() -> impl Strategy<Value = Point> {
676            (any::<f64>(), any::<f64>()).prop_map(|(x, y)| Point { x, y })
677        }
678
679        fn arb_color() -> impl Strategy<Value = Color> {
680            prop_oneof![
681                Just(Color::Red),
682                Just(Color::Green),
683                (any::<u8>(), any::<u8>(), any::<u8>())
684                    .prop_map(|(r, g, b)| Color::Custom(r, g, b)),
685            ]
686        }
687
688        proptest! {
689            #[test]
690            fn point_roundtrip(old in arb_point(), new in arb_point()) {
691                let d = generic_diff(&old, &new);
692                let patched = generic_patch(&old, &d);
693                prop_assert_eq!(patched, new);
694            }
695
696            #[test]
697            fn point_self_diff_is_empty(p in arb_point()) {
698                let d = generic_diff(&p, &p);
699                prop_assert!(d.is_empty());
700            }
701
702            #[test]
703            fn point_self_patch_is_identity(p in arb_point()) {
704                let d = generic_diff(&p, &p);
705                let patched = generic_patch(&p, &d);
706                prop_assert_eq!(patched, p);
707            }
708
709            #[test]
710            fn color_roundtrip(old in arb_color(), new in arb_color()) {
711                let d = generic_diff(&old, &new);
712                let patched = generic_patch(&old, &d);
713                prop_assert_eq!(patched, new);
714            }
715
716            #[test]
717            fn color_self_diff_is_empty(c in arb_color()) {
718                let d = generic_diff(&c, &c);
719                prop_assert!(d.is_empty());
720            }
721
722            #[test]
723            fn color_self_patch_is_identity(c in arb_color()) {
724                let d = generic_diff(&c, &c);
725                let patched = generic_patch(&c, &d);
726                prop_assert_eq!(patched, c);
727            }
728
729            #[test]
730            fn leaf_i32_roundtrip(old in any::<i32>(), new in any::<i32>()) {
731                let d = i32::diff(&old, &new);
732                let patched = i32::patch(&old, &d);
733                prop_assert_eq!(patched, new);
734            }
735
736            #[test]
737            fn leaf_i32_self_diff_empty(v in any::<i32>()) {
738                let d = i32::diff(&v, &v);
739                prop_assert!(d.is_empty());
740                prop_assert_eq!(d.change_count(), 0);
741            }
742
743            #[test]
744            fn product_u32_triple_roundtrip(
745                a0 in any::<u32>(), a1 in any::<u32>(), a2 in any::<u32>(),
746                b0 in any::<u32>(), b1 in any::<u32>(), b2 in any::<u32>(),
747            ) {
748                let old = Product(a0, Product(a1, Product(a2, Unit)));
749                let new = Product(b0, Product(b1, Product(b2, Unit)));
750                let d = <Product<u32, Product<u32, Product<u32, Unit>>>>::diff(&old, &new);
751                let patched = <Product<u32, Product<u32, Product<u32, Unit>>>>::patch(&old, &d);
752                prop_assert_eq!(patched, new);
753            }
754
755            #[test]
756            fn sum_roundtrip(
757                old_left in any::<bool>(), old_val in any::<u32>(),
758                new_left in any::<bool>(), new_val in any::<u32>(),
759            ) {
760                let old: Sum<u32, u32> = if old_left { Sum::Left(old_val) } else { Sum::Right(old_val) };
761                let new: Sum<u32, u32> = if new_left { Sum::Left(new_val) } else { Sum::Right(new_val) };
762                let d = <Sum<u32, u32>>::diff(&old, &new);
763                let patched = <Sum<u32, u32>>::patch(&old, &d);
764                prop_assert_eq!(patched, new);
765            }
766
767            #[test]
768            fn change_count_bounded_by_fields(
769                old in arb_point(), new in arb_point()
770            ) {
771                let d = generic_diff(&old, &new);
772                prop_assert!(d.change_count() <= 2);
773            }
774
775            #[test]
776            fn diff_same_implies_zero_changes(p in arb_point()) {
777                let d = generic_diff(&p, &p);
778                prop_assert_eq!(d.change_count(), 0);
779            }
780        }
781    }
782}