rumpsteak_types/
contentable.rs

1//! Contentable Trait for Canonical Serialization
2//!
3//! This module provides the `Contentable` trait for types that can be
4//! serialized to a canonical byte representation suitable for content addressing.
5//!
6//! # Design
7//!
8//! The serialization process:
9//! 1. Convert to de Bruijn representation (for α-equivalence)
10//! 2. Normalize branch ordering (deterministic)
11//! 3. Serialize to bytes (JSON by default, DAG-CBOR with feature flag)
12//!
13//! # Serialization Formats
14//!
15//! - **JSON** (default): Simple and human-readable. Uses `to_bytes`/`from_bytes`.
16//! - **DAG-CBOR** (with `dag-cbor` feature): Compact binary format compatible
17//!   with IPLD/IPFS. Uses `to_cbor_bytes`/`from_cbor_bytes`.
18//!
19//! # Lean Correspondence
20//!
21//! This module corresponds to `lean/Rumpsteak/Protocol/Serialize.lean`.
22//! The `toCbor`/`fromCbor` methods in Lean map to `to_cbor_bytes`/`from_cbor_bytes` here.
23
24use crate::content_id::{ContentId, Hasher, Sha256Hasher};
25use crate::de_bruijn::{GlobalTypeDB, LocalTypeRDB};
26use crate::{GlobalType, Label, LocalTypeR, PayloadSort};
27use serde::{de::DeserializeOwned, Serialize};
28
29/// Trait for types with canonical serialization.
30///
31/// Types implementing `Contentable` can be serialized to bytes in a
32/// deterministic way, enabling content addressing and structural comparison.
33///
34/// # Invariants
35///
36/// - `from_bytes(to_bytes(x)) ≈ x` (modulo α-equivalence for types with binders)
37/// - Two α-equivalent values produce identical bytes
38/// - Byte order is deterministic (independent of insertion order, etc.)
39///
40/// # Examples
41///
42/// ```
43/// use rumpsteak_types::{GlobalType, Label};
44/// use rumpsteak_types::contentable::Contentable;
45///
46/// // α-equivalent types produce the same bytes
47/// let g1 = GlobalType::mu("x", GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")));
48/// let g2 = GlobalType::mu("y", GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")));
49///
50/// assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
51/// ```
52pub trait Contentable: Sized {
53    /// Serialize to canonical byte representation (JSON format).
54    fn to_bytes(&self) -> Result<Vec<u8>, ContentableError>;
55
56    /// Deserialize from JSON bytes.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if deserialization fails.
61    fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
62
63    /// Serialize to DAG-CBOR bytes (requires `dag-cbor` feature).
64    ///
65    /// DAG-CBOR is a deterministic subset of CBOR designed for content addressing.
66    /// It produces more compact output than JSON and is compatible with IPLD/IPFS.
67    #[cfg(feature = "dag-cbor")]
68    fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError>;
69
70    /// Deserialize from DAG-CBOR bytes (requires `dag-cbor` feature).
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if deserialization fails.
75    #[cfg(feature = "dag-cbor")]
76    fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
77
78    /// Compute content ID using the specified hasher (from JSON bytes).
79    fn content_id<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
80        let bytes = self.to_bytes()?;
81        Ok(ContentId::from_bytes(&bytes))
82    }
83
84    /// Compute content ID using default SHA-256 hasher (from JSON bytes).
85    fn content_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
86        self.content_id()
87    }
88
89    /// Compute content ID from DAG-CBOR bytes (requires `dag-cbor` feature).
90    ///
91    /// This produces a different content ID than the JSON-based methods.
92    /// Use this for IPLD/IPFS compatibility.
93    #[cfg(feature = "dag-cbor")]
94    fn content_id_cbor<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
95        let bytes = self.to_cbor_bytes()?;
96        Ok(ContentId::from_bytes(&bytes))
97    }
98
99    /// Compute content ID from DAG-CBOR using SHA-256 (requires `dag-cbor` feature).
100    #[cfg(feature = "dag-cbor")]
101    fn content_id_cbor_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
102        self.content_id_cbor()
103    }
104}
105
106/// Errors that can occur during contentable operations.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum ContentableError {
109    /// Failed to deserialize bytes
110    DeserializationFailed(String),
111    /// Failed to serialize value
112    SerializationFailed(String),
113    /// Invalid format or structure
114    InvalidFormat(String),
115}
116
117impl std::fmt::Display for ContentableError {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        match self {
120            ContentableError::DeserializationFailed(msg) => {
121                write!(f, "deserialization failed: {msg}")
122            }
123            ContentableError::SerializationFailed(msg) => {
124                write!(f, "serialization failed: {msg}")
125            }
126            ContentableError::InvalidFormat(msg) => {
127                write!(f, "invalid format: {msg}")
128            }
129        }
130    }
131}
132
133impl std::error::Error for ContentableError {}
134
135// Helper for JSON serialization
136fn to_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
137    // Use compact JSON without pretty printing for determinism
138    serde_json::to_vec(value).map_err(|e| ContentableError::SerializationFailed(e.to_string()))
139}
140
141fn from_json_bytes<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
142    serde_json::from_slice(bytes)
143        .map_err(|e| ContentableError::DeserializationFailed(e.to_string()))
144}
145
146// Helper for DAG-CBOR serialization (requires dag-cbor feature)
147#[cfg(feature = "dag-cbor")]
148fn to_cbor_bytes_impl<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
149    serde_ipld_dagcbor::to_vec(value)
150        .map_err(|e| ContentableError::SerializationFailed(format!("dag-cbor: {e}")))
151}
152
153#[cfg(feature = "dag-cbor")]
154fn from_cbor_bytes_impl<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
155    serde_ipld_dagcbor::from_slice(bytes)
156        .map_err(|e| ContentableError::DeserializationFailed(format!("dag-cbor: {e}")))
157}
158
159// ============================================================================
160// Contentable implementations
161// ============================================================================
162
163impl Contentable for PayloadSort {
164    fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
165        to_json_bytes(self)
166    }
167
168    fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
169        from_json_bytes(bytes)
170    }
171
172    #[cfg(feature = "dag-cbor")]
173    fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
174        to_cbor_bytes_impl(self)
175    }
176
177    #[cfg(feature = "dag-cbor")]
178    fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
179        from_cbor_bytes_impl(bytes)
180    }
181}
182
183impl Contentable for Label {
184    fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
185        to_json_bytes(self)
186    }
187
188    fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
189        from_json_bytes(bytes)
190    }
191
192    #[cfg(feature = "dag-cbor")]
193    fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
194        to_cbor_bytes_impl(self)
195    }
196
197    #[cfg(feature = "dag-cbor")]
198    fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
199        from_cbor_bytes_impl(bytes)
200    }
201}
202
203impl Contentable for GlobalType {
204    fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
205        // Convert to de Bruijn, normalize, then serialize
206        let db = GlobalTypeDB::from(self).normalize();
207        to_json_bytes(&db)
208    }
209
210    fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
211        // Note: This returns a type with generated variable names,
212        // since de Bruijn indices don't preserve names.
213        let db: GlobalTypeDB = from_json_bytes(bytes)?;
214        Ok(global_from_de_bruijn(&db, &mut vec![]))
215    }
216
217    #[cfg(feature = "dag-cbor")]
218    fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
219        let db = GlobalTypeDB::from(self).normalize();
220        to_cbor_bytes_impl(&db)
221    }
222
223    #[cfg(feature = "dag-cbor")]
224    fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
225        let db: GlobalTypeDB = from_cbor_bytes_impl(bytes)?;
226        Ok(global_from_de_bruijn(&db, &mut vec![]))
227    }
228}
229
230impl Contentable for LocalTypeR {
231    fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
232        // Convert to de Bruijn, normalize, then serialize
233        let db = LocalTypeRDB::from(self).normalize();
234        to_json_bytes(&db)
235    }
236
237    fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
238        // Note: This returns a type with generated variable names,
239        // since de Bruijn indices don't preserve names.
240        let db: LocalTypeRDB = from_json_bytes(bytes)?;
241        Ok(local_from_de_bruijn(&db, &mut vec![]))
242    }
243
244    #[cfg(feature = "dag-cbor")]
245    fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
246        let db = LocalTypeRDB::from(self).normalize();
247        to_cbor_bytes_impl(&db)
248    }
249
250    #[cfg(feature = "dag-cbor")]
251    fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
252        let db: LocalTypeRDB = from_cbor_bytes_impl(bytes)?;
253        Ok(local_from_de_bruijn(&db, &mut vec![]))
254    }
255}
256
257// ============================================================================
258// De Bruijn back-conversion (generates fresh variable names)
259// ============================================================================
260
261fn global_from_de_bruijn(db: &GlobalTypeDB, names: &mut Vec<String>) -> GlobalType {
262    match db {
263        GlobalTypeDB::End => GlobalType::End,
264        GlobalTypeDB::Comm {
265            sender,
266            receiver,
267            branches,
268        } => GlobalType::Comm {
269            sender: sender.clone(),
270            receiver: receiver.clone(),
271            branches: branches
272                .iter()
273                .map(|(l, cont)| (l.clone(), global_from_de_bruijn(cont, names)))
274                .collect(),
275        },
276        GlobalTypeDB::Rec(body) => {
277            // Generate a fresh variable name
278            let var_name = format!("t{}", names.len());
279            names.push(var_name.clone());
280            let body_converted = global_from_de_bruijn(body, names);
281            names.pop();
282            GlobalType::Mu {
283                var: var_name,
284                body: Box::new(body_converted),
285            }
286        }
287        GlobalTypeDB::Var(idx) => {
288            // Look up the variable name from the environment
289            let name = names
290                .get(names.len().saturating_sub(1 + idx))
291                .cloned()
292                .unwrap_or_else(|| format!("free{idx}"));
293            GlobalType::Var(name)
294        }
295    }
296}
297
298fn local_from_de_bruijn(db: &LocalTypeRDB, names: &mut Vec<String>) -> LocalTypeR {
299    match db {
300        LocalTypeRDB::End => LocalTypeR::End,
301        LocalTypeRDB::Send { partner, branches } => LocalTypeR::Send {
302            partner: partner.clone(),
303            branches: branches
304                .iter()
305                .map(|(l, cont)| (l.clone(), local_from_de_bruijn(cont, names)))
306                .collect(),
307        },
308        LocalTypeRDB::Recv { partner, branches } => LocalTypeR::Recv {
309            partner: partner.clone(),
310            branches: branches
311                .iter()
312                .map(|(l, cont)| (l.clone(), local_from_de_bruijn(cont, names)))
313                .collect(),
314        },
315        LocalTypeRDB::Rec(body) => {
316            // Generate a fresh variable name
317            let var_name = format!("t{}", names.len());
318            names.push(var_name.clone());
319            let body_converted = local_from_de_bruijn(body, names);
320            names.pop();
321            LocalTypeR::Mu {
322                var: var_name,
323                body: Box::new(body_converted),
324            }
325        }
326        LocalTypeRDB::Var(idx) => {
327            // Look up the variable name from the environment
328            let name = names
329                .get(names.len().saturating_sub(1 + idx))
330                .cloned()
331                .unwrap_or_else(|| format!("free{idx}"));
332            LocalTypeR::Var(name)
333        }
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_payload_sort_roundtrip() {
343        let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
344        let bytes = sort.to_bytes().unwrap();
345        let recovered = PayloadSort::from_bytes(&bytes).unwrap();
346        assert_eq!(sort, recovered);
347    }
348
349    #[test]
350    fn test_label_roundtrip() {
351        let label = Label::with_sort("data", PayloadSort::Nat);
352        let bytes = label.to_bytes().unwrap();
353        let recovered = Label::from_bytes(&bytes).unwrap();
354        assert_eq!(label, recovered);
355    }
356
357    #[test]
358    fn test_global_type_alpha_equivalence() {
359        // μx. A → B : msg. x
360        let g1 = GlobalType::mu(
361            "x",
362            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
363        );
364        // μy. A → B : msg. y (same structure, different variable name)
365        let g2 = GlobalType::mu(
366            "y",
367            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
368        );
369
370        // α-equivalent types should produce the same bytes
371        assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
372
373        // And the same content ID
374        assert_eq!(
375            g1.content_id_sha256().unwrap(),
376            g2.content_id_sha256().unwrap()
377        );
378    }
379
380    #[test]
381    fn test_local_type_alpha_equivalence() {
382        // μx. !B{msg.x}
383        let t1 = LocalTypeR::mu(
384            "x",
385            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
386        );
387        // μy. !B{msg.y}
388        let t2 = LocalTypeR::mu(
389            "y",
390            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("y")),
391        );
392
393        assert_eq!(t1.to_bytes().unwrap(), t2.to_bytes().unwrap());
394        assert_eq!(
395            t1.content_id_sha256().unwrap(),
396            t2.content_id_sha256().unwrap()
397        );
398    }
399
400    #[test]
401    fn test_global_type_roundtrip() {
402        let g = GlobalType::mu(
403            "x",
404            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
405        );
406
407        let bytes = g.to_bytes().unwrap();
408        let recovered = GlobalType::from_bytes(&bytes).unwrap();
409
410        // Roundtrip should be α-equivalent (same structure, possibly different names)
411        assert_eq!(g.to_bytes().unwrap(), recovered.to_bytes().unwrap());
412    }
413
414    #[test]
415    fn test_local_type_roundtrip() {
416        let t = LocalTypeR::mu(
417            "x",
418            LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
419        );
420
421        let bytes = t.to_bytes().unwrap();
422        let recovered = LocalTypeR::from_bytes(&bytes).unwrap();
423
424        assert_eq!(t.to_bytes().unwrap(), recovered.to_bytes().unwrap());
425    }
426
427    #[test]
428    fn test_branch_ordering_normalized() {
429        // Branches in different order should produce same bytes
430        let g1 = GlobalType::comm(
431            "A",
432            "B",
433            vec![
434                (Label::new("b"), GlobalType::End),
435                (Label::new("a"), GlobalType::End),
436            ],
437        );
438        let g2 = GlobalType::comm(
439            "A",
440            "B",
441            vec![
442                (Label::new("a"), GlobalType::End),
443                (Label::new("b"), GlobalType::End),
444            ],
445        );
446
447        assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
448    }
449
450    #[test]
451    fn test_different_types_different_bytes() {
452        let g1 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
453        let g2 = GlobalType::send("A", "B", Label::new("other"), GlobalType::End);
454
455        assert_ne!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
456        assert_ne!(
457            g1.content_id_sha256().unwrap(),
458            g2.content_id_sha256().unwrap()
459        );
460    }
461
462    #[test]
463    fn test_nested_recursion_content_id() {
464        // μx. μy. A → B : msg. y
465        let g1 = GlobalType::mu(
466            "x",
467            GlobalType::mu(
468                "y",
469                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
470            ),
471        );
472        // μa. μb. A → B : msg. b
473        let g2 = GlobalType::mu(
474            "a",
475            GlobalType::mu(
476                "b",
477                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("b")),
478            ),
479        );
480
481        assert_eq!(
482            g1.content_id_sha256().unwrap(),
483            g2.content_id_sha256().unwrap()
484        );
485    }
486
487    #[test]
488    fn test_different_binder_reference() {
489        // μx. μy. A → B : msg. x (references OUTER binder)
490        let g1 = GlobalType::mu(
491            "x",
492            GlobalType::mu(
493                "y",
494                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
495            ),
496        );
497        // μx. μy. A → B : msg. y (references INNER binder)
498        let g2 = GlobalType::mu(
499            "x",
500            GlobalType::mu(
501                "y",
502                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
503            ),
504        );
505
506        // These are NOT α-equivalent
507        assert_ne!(
508            g1.content_id_sha256().unwrap(),
509            g2.content_id_sha256().unwrap()
510        );
511    }
512
513    // ========================================================================
514    // DAG-CBOR tests (require dag-cbor feature)
515    // ========================================================================
516
517    #[cfg(feature = "dag-cbor")]
518    mod cbor_tests {
519        use super::*;
520
521        #[test]
522        fn test_payload_sort_cbor_roundtrip() {
523            let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
524            let bytes = sort.to_cbor_bytes().unwrap();
525            let recovered = PayloadSort::from_cbor_bytes(&bytes).unwrap();
526            assert_eq!(sort, recovered);
527        }
528
529        #[test]
530        fn test_label_cbor_roundtrip() {
531            let label = Label::with_sort("data", PayloadSort::Nat);
532            let bytes = label.to_cbor_bytes().unwrap();
533            let recovered = Label::from_cbor_bytes(&bytes).unwrap();
534            assert_eq!(label, recovered);
535        }
536
537        #[test]
538        fn test_global_type_cbor_roundtrip() {
539            let g = GlobalType::mu(
540                "x",
541                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
542            );
543
544            let bytes = g.to_cbor_bytes().unwrap();
545            let recovered = GlobalType::from_cbor_bytes(&bytes).unwrap();
546
547            // Roundtrip should be α-equivalent
548            assert_eq!(
549                g.to_cbor_bytes().unwrap(),
550                recovered.to_cbor_bytes().unwrap()
551            );
552        }
553
554        #[test]
555        fn test_local_type_cbor_roundtrip() {
556            let t = LocalTypeR::mu(
557                "x",
558                LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
559            );
560
561            let bytes = t.to_cbor_bytes().unwrap();
562            let recovered = LocalTypeR::from_cbor_bytes(&bytes).unwrap();
563
564            assert_eq!(
565                t.to_cbor_bytes().unwrap(),
566                recovered.to_cbor_bytes().unwrap()
567            );
568        }
569
570        #[test]
571        fn test_cbor_alpha_equivalence() {
572            // Two α-equivalent types should produce the same CBOR bytes
573            let g1 = GlobalType::mu(
574                "x",
575                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
576            );
577            let g2 = GlobalType::mu(
578                "y",
579                GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
580            );
581
582            assert_eq!(g1.to_cbor_bytes().unwrap(), g2.to_cbor_bytes().unwrap());
583            assert_eq!(
584                g1.content_id_cbor_sha256().unwrap(),
585                g2.content_id_cbor_sha256().unwrap()
586            );
587        }
588
589        #[test]
590        fn test_cbor_more_compact_than_json() {
591            // CBOR should typically be more compact than JSON
592            let g = GlobalType::comm(
593                "A",
594                "B",
595                vec![
596                    (Label::new("msg1"), GlobalType::End),
597                    (Label::new("msg2"), GlobalType::End),
598                    (Label::new("msg3"), GlobalType::End),
599                ],
600            );
601
602            let json_bytes = g.to_bytes().unwrap();
603            let cbor_bytes = g.to_cbor_bytes().unwrap();
604
605            // CBOR is typically 30-50% smaller than JSON for structured data
606            assert!(
607                cbor_bytes.len() < json_bytes.len(),
608                "CBOR ({} bytes) should be smaller than JSON ({} bytes)",
609                cbor_bytes.len(),
610                json_bytes.len()
611            );
612        }
613
614        #[test]
615        fn test_json_and_cbor_produce_different_bytes() {
616            // JSON and CBOR are different formats, so bytes should differ
617            let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
618
619            let json_bytes = g.to_bytes().unwrap();
620            let cbor_bytes = g.to_cbor_bytes().unwrap();
621
622            assert_ne!(json_bytes, cbor_bytes);
623        }
624    }
625}
626
627// ============================================================================
628// Property-based tests for α-equivalence
629// ============================================================================
630
631#[cfg(test)]
632mod proptests {
633    use super::*;
634    use proptest::prelude::*;
635
636    /// Generate a random variable name from a small set
637    fn arb_var_name() -> impl Strategy<Value = String> {
638        prop_oneof![
639            Just("x".to_string()),
640            Just("y".to_string()),
641            Just("z".to_string()),
642            Just("t".to_string()),
643            Just("s".to_string()),
644        ]
645    }
646
647    /// Generate a random role name
648    fn arb_role() -> impl Strategy<Value = String> {
649        prop_oneof![
650            Just("A".to_string()),
651            Just("B".to_string()),
652            Just("C".to_string()),
653        ]
654    }
655
656    /// Generate a random label
657    fn arb_label() -> impl Strategy<Value = Label> {
658        prop_oneof![
659            Just(Label::new("msg")),
660            Just(Label::new("data")),
661            Just(Label::new("ack")),
662            Just(Label::with_sort("value", PayloadSort::Nat)),
663            Just(Label::with_sort("flag", PayloadSort::Bool)),
664        ]
665    }
666
667    /// Generate a random GlobalType (limited depth to avoid explosion)
668    fn arb_global_type(depth: usize) -> impl Strategy<Value = GlobalType> {
669        if depth == 0 {
670            prop_oneof![
671                Just(GlobalType::End),
672                arb_var_name().prop_map(GlobalType::var),
673            ]
674            .boxed()
675        } else {
676            prop_oneof![
677                Just(GlobalType::End),
678                // Simple send
679                (
680                    arb_role(),
681                    arb_role(),
682                    arb_label(),
683                    arb_global_type(depth - 1)
684                )
685                    .prop_map(|(sender, receiver, label, cont)| {
686                        GlobalType::send(sender, receiver, label, cont)
687                    }),
688                // Recursive type
689                (arb_var_name(), arb_global_type(depth - 1))
690                    .prop_map(|(var, body)| GlobalType::mu(var, body)),
691                // Variable
692                arb_var_name().prop_map(GlobalType::var),
693            ]
694            .boxed()
695        }
696    }
697
698    /// Generate a random LocalTypeR (limited depth)
699    #[allow(dead_code)]
700    fn arb_local_type(depth: usize) -> impl Strategy<Value = LocalTypeR> {
701        if depth == 0 {
702            prop_oneof![
703                Just(LocalTypeR::End),
704                arb_var_name().prop_map(LocalTypeR::var),
705            ]
706            .boxed()
707        } else {
708            prop_oneof![
709                Just(LocalTypeR::End),
710                // Simple send
711                (arb_role(), arb_label(), arb_local_type(depth - 1))
712                    .prop_map(|(partner, label, cont)| LocalTypeR::send(partner, label, cont)),
713                // Simple recv
714                (arb_role(), arb_label(), arb_local_type(depth - 1))
715                    .prop_map(|(partner, label, cont)| LocalTypeR::recv(partner, label, cont)),
716                // Recursive type
717                (arb_var_name(), arb_local_type(depth - 1))
718                    .prop_map(|(var, body)| LocalTypeR::mu(var, body)),
719                // Variable
720                arb_var_name().prop_map(LocalTypeR::var),
721            ]
722            .boxed()
723        }
724    }
725
726    /// Rename all bound variables in a GlobalType using a mapping
727    fn rename_global_type(g: &GlobalType, mapping: &[(&str, &str)]) -> GlobalType {
728        fn rename_inner(
729            g: &GlobalType,
730            mapping: &[(&str, &str)],
731            bound: &mut Vec<(String, String)>,
732        ) -> GlobalType {
733            match g {
734                GlobalType::End => GlobalType::End,
735                GlobalType::Comm {
736                    sender,
737                    receiver,
738                    branches,
739                } => GlobalType::Comm {
740                    sender: sender.clone(),
741                    receiver: receiver.clone(),
742                    branches: branches
743                        .iter()
744                        .map(|(l, cont)| (l.clone(), rename_inner(cont, mapping, bound)))
745                        .collect(),
746                },
747                GlobalType::Mu { var, body } => {
748                    // Find new name for this variable
749                    let new_var = mapping
750                        .iter()
751                        .find(|(old, _)| *old == var)
752                        .map(|(_, new)| new.to_string())
753                        .unwrap_or_else(|| var.clone());
754
755                    bound.push((var.clone(), new_var.clone()));
756                    let new_body = rename_inner(body, mapping, bound);
757                    bound.pop();
758
759                    GlobalType::Mu {
760                        var: new_var,
761                        body: Box::new(new_body),
762                    }
763                }
764                GlobalType::Var(name) => {
765                    // Check if this is a bound variable that was renamed
766                    let new_name = bound
767                        .iter()
768                        .rev()
769                        .find(|(old, _)| old == name)
770                        .map(|(_, new)| new.clone())
771                        .unwrap_or_else(|| name.clone());
772                    GlobalType::Var(new_name)
773                }
774            }
775        }
776        rename_inner(g, mapping, &mut vec![])
777    }
778
779    /// Generate a CLOSED global type (no free variables)
780    /// Uses a fixed variable name to ensure the body only references the bound var
781    fn arb_closed_global_type(depth: usize) -> impl Strategy<Value = GlobalType> {
782        // Use a fixed variable name for the binder
783        arb_var_name().prop_flat_map(move |var| {
784            let var_clone = var.clone();
785            arb_global_type_closed_body(depth, var)
786                .prop_map(move |body| GlobalType::mu(var_clone.clone(), body))
787        })
788    }
789
790    /// Generate a global type body that only references the given bound variable
791    fn arb_global_type_closed_body(
792        depth: usize,
793        bound_var: String,
794    ) -> impl Strategy<Value = GlobalType> {
795        if depth == 0 {
796            prop_oneof![
797                Just(GlobalType::End),
798                Just(GlobalType::var(bound_var)), // Reference the bound variable
799            ]
800            .boxed()
801        } else {
802            let bv = bound_var.clone();
803            let bv2 = bound_var.clone();
804            prop_oneof![
805                Just(GlobalType::End),
806                Just(GlobalType::var(bv)),
807                // Simple send
808                (arb_role(), arb_role(), arb_label()).prop_flat_map(
809                    move |(sender, receiver, label)| {
810                        let bv_inner = bv2.clone();
811                        arb_global_type_closed_body(depth - 1, bv_inner).prop_map(move |cont| {
812                            GlobalType::send(sender.clone(), receiver.clone(), label.clone(), cont)
813                        })
814                    }
815                ),
816            ]
817            .boxed()
818        }
819    }
820
821    proptest! {
822        /// Property: Same type produces same content ID
823        #[test]
824        fn prop_content_id_deterministic(g in arb_global_type(3)) {
825            let cid1 = g.content_id_sha256().unwrap();
826            let cid2 = g.content_id_sha256().unwrap();
827            prop_assert_eq!(cid1, cid2);
828        }
829
830        /// Property: Same type produces same bytes
831        #[test]
832        fn prop_to_bytes_deterministic(g in arb_global_type(3)) {
833            let bytes1 = g.to_bytes().unwrap();
834            let bytes2 = g.to_bytes().unwrap();
835            prop_assert_eq!(bytes1, bytes2);
836        }
837
838        /// Property: α-equivalent CLOSED types produce same content ID
839        /// (Free variables are NOT subject to α-equivalence)
840        #[test]
841        fn prop_alpha_equivalence_closed(g in arb_closed_global_type(3)) {
842            // Rename bound variable x → y throughout the type
843            let renamed = rename_global_type(&g, &[("x", "renamed_x"), ("y", "renamed_y"), ("t", "renamed_t")]);
844
845            // α-equivalent closed types should have same content ID
846            prop_assert_eq!(
847                g.content_id_sha256().unwrap(),
848                renamed.content_id_sha256().unwrap(),
849                "α-equivalent closed types should have same content ID"
850            );
851        }
852
853        /// Property: roundtrip preserves content ID for well-formed types
854        #[test]
855        fn prop_roundtrip_closed(g in arb_closed_global_type(3)) {
856            let bytes = g.to_bytes().unwrap();
857            if let Ok(recovered) = GlobalType::from_bytes(&bytes) {
858                // Roundtrip should preserve content ID (α-equivalence)
859                prop_assert_eq!(
860                    g.content_id_sha256().unwrap(),
861                    recovered.content_id_sha256().unwrap(),
862                    "roundtrip should preserve content ID for closed types"
863                );
864            }
865        }
866
867        /// Property: branch order doesn't affect content ID
868        #[test]
869        fn prop_branch_order_invariant(
870            sender in arb_role(),
871            receiver in arb_role(),
872            label1 in arb_label(),
873            label2 in arb_label(),
874        ) {
875            // Different label order
876            let g1 = GlobalType::comm(
877                &sender, &receiver,
878                vec![
879                    (label1.clone(), GlobalType::End),
880                    (label2.clone(), GlobalType::End),
881                ],
882            );
883            let g2 = GlobalType::comm(
884                &sender, &receiver,
885                vec![
886                    (label2, GlobalType::End),
887                    (label1, GlobalType::End),
888                ],
889            );
890
891            // Same content ID regardless of branch order
892            prop_assert_eq!(
893                g1.content_id_sha256().unwrap(),
894                g2.content_id_sha256().unwrap(),
895                "branch order should not affect content ID"
896            );
897        }
898
899        /// Property: LocalTypeR α-equivalence
900        #[test]
901        fn prop_local_type_alpha_equiv(
902            partner in arb_role(),
903            label in arb_label(),
904        ) {
905            let t1 = LocalTypeR::mu("x", LocalTypeR::send(&partner, label.clone(), LocalTypeR::var("x")));
906            let t2 = LocalTypeR::mu("y", LocalTypeR::send(&partner, label, LocalTypeR::var("y")));
907
908            prop_assert_eq!(
909                t1.content_id_sha256().unwrap(),
910                t2.content_id_sha256().unwrap(),
911                "α-equivalent local types should have same content ID"
912            );
913        }
914    }
915}