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