Skip to main content

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