1#[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
40pub const MAX_CONTENTABLE_BYTES: usize = MAX_MESSAGE_LEN_BYTES as usize;
42pub const MAX_CONTENTABLE_RECURSION_DEPTH_COUNT: usize = 256;
44
45pub trait Contentable: Sized {
72 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError>;
78
79 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
89
90 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 fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
117 self.to_bytes()
118 }
119
120 #[cfg(feature = "dag-cbor")]
129 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError>;
130
131 #[cfg(feature = "dag-cbor")]
137 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
138
139 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 fn content_id_default(&self) -> Result<ContentId<DefaultContentHasher>, ContentableError> {
155 self.content_id()
156 }
157
158 fn content_id_blake3(&self) -> Result<ContentId<Blake3Hasher>, ContentableError> {
164 self.content_id()
165 }
166
167 #[cfg(feature = "sha256")]
173 fn content_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
174 self.content_id()
175 }
176
177 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 fn template_id_default(&self) -> Result<ContentId<DefaultContentHasher>, ContentableError> {
193 self.template_id()
194 }
195
196 fn template_id_blake3(&self) -> Result<ContentId<Blake3Hasher>, ContentableError> {
202 self.template_id()
203 }
204
205 #[cfg(feature = "sha256")]
211 fn template_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
212 self.template_id()
213 }
214
215 #[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 #[cfg(feature = "dag-cbor")]
236 fn content_id_cbor_default(&self) -> Result<ContentId<DefaultContentHasher>, ContentableError> {
237 self.content_id_cbor()
238 }
239
240 #[cfg(feature = "dag-cbor")]
246 fn content_id_cbor_blake3(&self) -> Result<ContentId<Blake3Hasher>, ContentableError> {
247 self.content_id_cbor()
248 }
249
250 #[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#[derive(Debug, Clone, PartialEq, Eq)]
263pub enum ContentableError {
264 DeserializationFailed(String),
266 SerializationFailed(String),
268 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
290fn to_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
292 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#[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
411impl 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 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 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 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 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
546fn 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 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 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 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 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 let g1 = GlobalType::mu(
714 "x",
715 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
716 );
717 let g2 = GlobalType::mu(
719 "y",
720 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
721 );
722
723 assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
725
726 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 let t1 = LocalTypeR::mu(
737 "x",
738 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
739 );
740 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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#[cfg(test)]
1056mod proptests {
1057 use super::*;
1058 use proptest::prelude::*;
1059
1060 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 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 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 #[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 (arb_role(), arb_label(), arb_local_type(depth - 1))
1105 .prop_map(|(partner, label, cont)| LocalTypeR::send(partner, label, cont)),
1106 (arb_role(), arb_label(), arb_local_type(depth - 1))
1108 .prop_map(|(partner, label, cont)| LocalTypeR::recv(partner, label, cont)),
1109 (arb_var_name(), arb_local_type(depth - 1))
1111 .prop_map(|(var, body)| LocalTypeR::mu(var, body)),
1112 arb_var_name().prop_map(LocalTypeR::var),
1114 ]
1115 .boxed()
1116 }
1117 }
1118
1119 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 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 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 fn arb_closed_global_type(depth: usize) -> impl Strategy<Value = GlobalType> {
1175 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 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)), ]
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 (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 #[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 #[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 #[test]
1234 fn prop_alpha_equivalence_closed(g in arb_closed_global_type(3)) {
1235 let renamed = rename_global_type(&g, &[("x", "renamed_x"), ("y", "renamed_y"), ("t", "renamed_t")]);
1237
1238 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 #[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 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 #[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 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 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 #[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}