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