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};
32use serde::{de::DeserializeOwned, Serialize};
33
34pub trait Contentable: Sized {
61 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError>;
67
68 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
74
75 fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
84 self.to_bytes()
85 }
86
87 #[cfg(feature = "dag-cbor")]
96 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError>;
97
98 #[cfg(feature = "dag-cbor")]
104 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
105
106 fn content_id<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
112 let bytes = self.to_bytes()?;
113 Ok(ContentId::from_bytes(&bytes))
114 }
115
116 fn content_id_default(&self) -> Result<ContentId<DefaultContentHasher>, ContentableError> {
122 self.content_id()
123 }
124
125 fn content_id_blake3(&self) -> Result<ContentId<Blake3Hasher>, ContentableError> {
131 self.content_id()
132 }
133
134 #[cfg(feature = "sha256")]
140 fn content_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
141 self.content_id()
142 }
143
144 fn template_id<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
150 let bytes = self.to_template_bytes()?;
151 Ok(ContentId::from_bytes(&bytes))
152 }
153
154 fn template_id_default(&self) -> Result<ContentId<DefaultContentHasher>, ContentableError> {
160 self.template_id()
161 }
162
163 fn template_id_blake3(&self) -> Result<ContentId<Blake3Hasher>, ContentableError> {
169 self.template_id()
170 }
171
172 #[cfg(feature = "sha256")]
178 fn template_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
179 self.template_id()
180 }
181
182 #[cfg(feature = "dag-cbor")]
191 fn content_id_cbor<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
192 let bytes = self.to_cbor_bytes()?;
193 Ok(ContentId::from_bytes(&bytes))
194 }
195
196 #[cfg(feature = "dag-cbor")]
203 fn content_id_cbor_default(&self) -> Result<ContentId<DefaultContentHasher>, ContentableError> {
204 self.content_id_cbor()
205 }
206
207 #[cfg(feature = "dag-cbor")]
213 fn content_id_cbor_blake3(&self) -> Result<ContentId<Blake3Hasher>, ContentableError> {
214 self.content_id_cbor()
215 }
216
217 #[cfg(all(feature = "dag-cbor", feature = "sha256"))]
223 fn content_id_cbor_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
224 self.content_id_cbor()
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
230pub enum ContentableError {
231 DeserializationFailed(String),
233 SerializationFailed(String),
235 InvalidFormat(String),
237}
238
239impl std::fmt::Display for ContentableError {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 ContentableError::DeserializationFailed(msg) => {
243 write!(f, "deserialization failed: {msg}")
244 }
245 ContentableError::SerializationFailed(msg) => {
246 write!(f, "serialization failed: {msg}")
247 }
248 ContentableError::InvalidFormat(msg) => {
249 write!(f, "invalid format: {msg}")
250 }
251 }
252 }
253}
254
255impl std::error::Error for ContentableError {}
256
257fn to_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
259 serde_json::to_vec(value).map_err(|e| ContentableError::SerializationFailed(e.to_string()))
261}
262
263fn from_json_bytes<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
264 serde_json::from_slice(bytes)
265 .map_err(|e| ContentableError::DeserializationFailed(e.to_string()))
266}
267
268fn sorted_free_vars(mut vars: Vec<String>) -> Vec<String> {
269 vars.sort();
270 vars.dedup();
271 vars
272}
273
274#[derive(Serialize)]
275struct GlobalTemplateEnvelope {
276 free_vars: Vec<String>,
277 db: GlobalTypeDB,
278}
279
280#[derive(Serialize)]
281struct LocalTemplateEnvelope {
282 free_vars: Vec<String>,
283 db: LocalTypeRDB,
284}
285
286#[cfg(feature = "dag-cbor")]
288fn to_cbor_bytes_impl<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
289 serde_ipld_dagcbor::to_vec(value)
290 .map_err(|e| ContentableError::SerializationFailed(format!("dag-cbor: {e}")))
291}
292
293#[cfg(feature = "dag-cbor")]
294fn from_cbor_bytes_impl<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
295 serde_ipld_dagcbor::from_slice(bytes)
296 .map_err(|e| ContentableError::DeserializationFailed(format!("dag-cbor: {e}")))
297}
298
299impl Contentable for PayloadSort {
304 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
305 to_json_bytes(self)
306 }
307
308 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
309 from_json_bytes(bytes)
310 }
311
312 #[cfg(feature = "dag-cbor")]
313 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
314 to_cbor_bytes_impl(self)
315 }
316
317 #[cfg(feature = "dag-cbor")]
318 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
319 from_cbor_bytes_impl(bytes)
320 }
321}
322
323impl Contentable for Label {
324 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
325 to_json_bytes(self)
326 }
327
328 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
329 from_json_bytes(bytes)
330 }
331
332 #[cfg(feature = "dag-cbor")]
333 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
334 to_cbor_bytes_impl(self)
335 }
336
337 #[cfg(feature = "dag-cbor")]
338 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
339 from_cbor_bytes_impl(bytes)
340 }
341}
342
343impl Contentable for GlobalType {
344 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
345 if !self.all_vars_bound() {
346 return Err(ContentableError::InvalidFormat(
347 "canonical serialization requires all recursion variables to be bound".to_string(),
348 ));
349 }
350 let db = GlobalTypeDB::from(self).normalize();
352 to_json_bytes(&db)
353 }
354
355 fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
356 let free_vars = sorted_free_vars(self.free_vars());
357 let env: Vec<&str> = free_vars.iter().map(String::as_str).collect();
358 let db = GlobalTypeDB::from_global_type_with_env(self, &env).normalize();
359 let envelope = GlobalTemplateEnvelope { free_vars, db };
360 to_json_bytes(&envelope)
361 }
362
363 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
364 let db: GlobalTypeDB = from_json_bytes(bytes)?;
367 Ok(global_from_de_bruijn(&db, &mut vec![]))
368 }
369
370 #[cfg(feature = "dag-cbor")]
371 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
372 if !self.all_vars_bound() {
373 return Err(ContentableError::InvalidFormat(
374 "canonical serialization requires all recursion variables to be bound".to_string(),
375 ));
376 }
377 let db = GlobalTypeDB::from(self).normalize();
378 to_cbor_bytes_impl(&db)
379 }
380
381 #[cfg(feature = "dag-cbor")]
382 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
383 let db: GlobalTypeDB = from_cbor_bytes_impl(bytes)?;
384 Ok(global_from_de_bruijn(&db, &mut vec![]))
385 }
386}
387
388impl Contentable for LocalTypeR {
389 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
390 if !self.all_vars_bound() {
391 return Err(ContentableError::InvalidFormat(
392 "canonical serialization requires all recursion variables to be bound".to_string(),
393 ));
394 }
395 let db = LocalTypeRDB::from(self).normalize();
398 to_json_bytes(&db)
399 }
400
401 fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
402 let free_vars = sorted_free_vars(self.free_vars());
403 let env: Vec<&str> = free_vars.iter().map(String::as_str).collect();
404 let db = LocalTypeRDB::from_local_type_with_env(self, &env).normalize();
405 let envelope = LocalTemplateEnvelope { free_vars, db };
406 to_json_bytes(&envelope)
407 }
408
409 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
410 let db: LocalTypeRDB = from_json_bytes(bytes)?;
413 Ok(local_from_de_bruijn(&db, &mut vec![]))
414 }
415
416 #[cfg(feature = "dag-cbor")]
417 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
418 if !self.all_vars_bound() {
419 return Err(ContentableError::InvalidFormat(
420 "canonical serialization requires all recursion variables to be bound".to_string(),
421 ));
422 }
423 let db = LocalTypeRDB::from(self).normalize();
424 to_cbor_bytes_impl(&db)
425 }
426
427 #[cfg(feature = "dag-cbor")]
428 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
429 let db: LocalTypeRDB = from_cbor_bytes_impl(bytes)?;
430 Ok(local_from_de_bruijn(&db, &mut vec![]))
431 }
432}
433
434fn global_from_de_bruijn(db: &GlobalTypeDB, names: &mut Vec<String>) -> GlobalType {
439 match db {
440 GlobalTypeDB::End => GlobalType::End,
441 GlobalTypeDB::Comm {
442 sender,
443 receiver,
444 branches,
445 } => GlobalType::Comm {
446 sender: sender.clone(),
447 receiver: receiver.clone(),
448 branches: branches
449 .iter()
450 .map(|(l, cont)| (l.clone(), global_from_de_bruijn(cont, names)))
451 .collect(),
452 },
453 GlobalTypeDB::Rec(body) => {
454 let var_name = format!("t{}", names.len());
456 names.push(var_name.clone());
457 let body_converted = global_from_de_bruijn(body, names);
458 names.pop();
459 GlobalType::Mu {
460 var: var_name,
461 body: Box::new(body_converted),
462 }
463 }
464 GlobalTypeDB::Var(idx) => {
465 let name = names
467 .get(names.len().saturating_sub(1 + idx))
468 .cloned()
469 .unwrap_or_else(|| format!("free{idx}"));
470 GlobalType::Var(name)
471 }
472 }
473}
474
475fn local_from_de_bruijn(db: &LocalTypeRDB, names: &mut Vec<String>) -> LocalTypeR {
476 match db {
477 LocalTypeRDB::End => LocalTypeR::End,
478 LocalTypeRDB::Send { partner, branches } => LocalTypeR::Send {
479 partner: partner.clone(),
480 branches: branches
481 .iter()
482 .map(|(l, vt, cont)| (l.clone(), vt.clone(), local_from_de_bruijn(cont, names)))
483 .collect(),
484 },
485 LocalTypeRDB::Recv { partner, branches } => LocalTypeR::Recv {
486 partner: partner.clone(),
487 branches: branches
488 .iter()
489 .map(|(l, vt, cont)| (l.clone(), vt.clone(), local_from_de_bruijn(cont, names)))
490 .collect(),
491 },
492 LocalTypeRDB::Rec(body) => {
493 let var_name = format!("t{}", names.len());
495 names.push(var_name.clone());
496 let body_converted = local_from_de_bruijn(body, names);
497 names.pop();
498 LocalTypeR::Mu {
499 var: var_name,
500 body: Box::new(body_converted),
501 }
502 }
503 LocalTypeRDB::Var(idx) => {
504 let name = names
506 .get(names.len().saturating_sub(1 + idx))
507 .cloned()
508 .unwrap_or_else(|| format!("free{idx}"));
509 LocalTypeR::Var(name)
510 }
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn test_default_content_id_helper() {
520 let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
521 let cid = g.content_id_default().unwrap();
522 assert_eq!(cid.algorithm(), "blake3");
523 }
524
525 #[test]
526 fn test_payload_sort_roundtrip() {
527 let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
528 let bytes = sort.to_bytes().unwrap();
529 let recovered = PayloadSort::from_bytes(&bytes).unwrap();
530 assert_eq!(sort, recovered);
531 }
532
533 #[test]
534 fn test_label_roundtrip() {
535 let label = Label::with_sort("data", PayloadSort::Nat);
536 let bytes = label.to_bytes().unwrap();
537 let recovered = Label::from_bytes(&bytes).unwrap();
538 assert_eq!(label, recovered);
539 }
540
541 #[test]
542 fn test_global_type_alpha_equivalence() {
543 let g1 = GlobalType::mu(
545 "x",
546 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
547 );
548 let g2 = GlobalType::mu(
550 "y",
551 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
552 );
553
554 assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
556
557 assert_eq!(
559 g1.content_id_default().unwrap(),
560 g2.content_id_default().unwrap()
561 );
562 }
563
564 #[test]
565 fn test_local_type_alpha_equivalence() {
566 let t1 = LocalTypeR::mu(
568 "x",
569 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
570 );
571 let t2 = LocalTypeR::mu(
573 "y",
574 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("y")),
575 );
576
577 assert_eq!(t1.to_bytes().unwrap(), t2.to_bytes().unwrap());
578 assert_eq!(
579 t1.content_id_default().unwrap(),
580 t2.content_id_default().unwrap()
581 );
582 }
583
584 #[test]
585 fn test_global_type_roundtrip() {
586 let g = GlobalType::mu(
587 "x",
588 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
589 );
590
591 let bytes = g.to_bytes().unwrap();
592 let recovered = GlobalType::from_bytes(&bytes).unwrap();
593
594 assert_eq!(g.to_bytes().unwrap(), recovered.to_bytes().unwrap());
596 }
597
598 #[test]
599 fn test_local_type_roundtrip() {
600 let t = LocalTypeR::mu(
601 "x",
602 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
603 );
604
605 let bytes = t.to_bytes().unwrap();
606 let recovered = LocalTypeR::from_bytes(&bytes).unwrap();
607
608 assert_eq!(t.to_bytes().unwrap(), recovered.to_bytes().unwrap());
609 }
610
611 #[test]
612 fn test_local_type_roundtrip_preserves_payload_annotation() {
613 let t = LocalTypeR::Send {
614 partner: "B".to_string(),
615 branches: vec![(
616 Label::new("msg"),
617 Some(crate::ValType::Nat),
618 LocalTypeR::Recv {
619 partner: "A".to_string(),
620 branches: vec![(
621 Label::new("ack"),
622 Some(crate::ValType::Bool),
623 LocalTypeR::End,
624 )],
625 },
626 )],
627 };
628
629 let bytes = t.to_bytes().unwrap();
630 let recovered = LocalTypeR::from_bytes(&bytes).unwrap();
631 assert_eq!(t, recovered);
632 }
633
634 #[test]
635 fn test_branch_ordering_normalized() {
636 let g1 = GlobalType::comm(
638 "A",
639 "B",
640 vec![
641 (Label::new("b"), GlobalType::End),
642 (Label::new("a"), GlobalType::End),
643 ],
644 );
645 let g2 = GlobalType::comm(
646 "A",
647 "B",
648 vec![
649 (Label::new("a"), GlobalType::End),
650 (Label::new("b"), GlobalType::End),
651 ],
652 );
653
654 assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
655 }
656
657 #[test]
658 fn test_different_types_different_bytes() {
659 let g1 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
660 let g2 = GlobalType::send("A", "B", Label::new("other"), GlobalType::End);
661
662 assert_ne!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
663 assert_ne!(
664 g1.content_id_default().unwrap(),
665 g2.content_id_default().unwrap()
666 );
667 }
668
669 #[test]
670 fn test_nested_recursion_content_id() {
671 let g1 = GlobalType::mu(
673 "x",
674 GlobalType::mu(
675 "y",
676 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
677 ),
678 );
679 let g2 = GlobalType::mu(
681 "a",
682 GlobalType::mu(
683 "b",
684 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("b")),
685 ),
686 );
687
688 assert_eq!(
689 g1.content_id_default().unwrap(),
690 g2.content_id_default().unwrap()
691 );
692 }
693
694 #[test]
695 fn test_different_binder_reference() {
696 let g1 = GlobalType::mu(
698 "x",
699 GlobalType::mu(
700 "y",
701 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
702 ),
703 );
704 let g2 = GlobalType::mu(
706 "x",
707 GlobalType::mu(
708 "y",
709 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
710 ),
711 );
712
713 assert_ne!(
715 g1.content_id_default().unwrap(),
716 g2.content_id_default().unwrap()
717 );
718 }
719
720 #[test]
721 fn test_global_type_open_term_rejected_for_canonical_serialization() {
722 let open = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("free_t"));
723 let err = open.to_bytes().expect_err("open terms must be rejected");
724 assert!(matches!(err, ContentableError::InvalidFormat(_)));
725 }
726
727 #[test]
728 fn test_local_type_open_term_rejected_for_canonical_serialization() {
729 let open = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("free_t"));
730 let err = open.to_bytes().expect_err("open terms must be rejected");
731 assert!(matches!(err, ContentableError::InvalidFormat(_)));
732 }
733
734 #[test]
735 fn test_global_type_open_term_has_template_id() {
736 let open = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("free_t"));
737 let tid = open
738 .template_id_default()
739 .expect("open terms should support template IDs");
740 let tid2 = open
741 .template_id_default()
742 .expect("template IDs should be deterministic");
743 assert_eq!(tid, tid2);
744 }
745
746 #[test]
747 fn test_local_type_open_term_has_template_id() {
748 let open = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("free_t"));
749 let tid = open
750 .template_id_default()
751 .expect("open terms should support template IDs");
752 let tid2 = open
753 .template_id_default()
754 .expect("template IDs should be deterministic");
755 assert_eq!(tid, tid2);
756 }
757
758 #[test]
759 fn test_template_id_distinguishes_free_variable_interfaces() {
760 let g1 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x"));
761 let g2 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y"));
762 assert_ne!(
763 g1.template_id_default().unwrap(),
764 g2.template_id_default().unwrap()
765 );
766 }
767
768 #[cfg(feature = "dag-cbor")]
773 mod cbor_tests {
774 use super::*;
775
776 #[test]
777 fn test_payload_sort_cbor_roundtrip() {
778 let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
779 let bytes = sort.to_cbor_bytes().unwrap();
780 let recovered = PayloadSort::from_cbor_bytes(&bytes).unwrap();
781 assert_eq!(sort, recovered);
782 }
783
784 #[test]
785 fn test_label_cbor_roundtrip() {
786 let label = Label::with_sort("data", PayloadSort::Nat);
787 let bytes = label.to_cbor_bytes().unwrap();
788 let recovered = Label::from_cbor_bytes(&bytes).unwrap();
789 assert_eq!(label, recovered);
790 }
791
792 #[test]
793 fn test_global_type_cbor_roundtrip() {
794 let g = GlobalType::mu(
795 "x",
796 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
797 );
798
799 let bytes = g.to_cbor_bytes().unwrap();
800 let recovered = GlobalType::from_cbor_bytes(&bytes).unwrap();
801
802 assert_eq!(
804 g.to_cbor_bytes().unwrap(),
805 recovered.to_cbor_bytes().unwrap()
806 );
807 }
808
809 #[test]
810 fn test_local_type_cbor_roundtrip() {
811 let t = LocalTypeR::mu(
812 "x",
813 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
814 );
815
816 let bytes = t.to_cbor_bytes().unwrap();
817 let recovered = LocalTypeR::from_cbor_bytes(&bytes).unwrap();
818
819 assert_eq!(
820 t.to_cbor_bytes().unwrap(),
821 recovered.to_cbor_bytes().unwrap()
822 );
823 }
824
825 #[test]
826 fn test_cbor_alpha_equivalence() {
827 let g1 = GlobalType::mu(
829 "x",
830 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
831 );
832 let g2 = GlobalType::mu(
833 "y",
834 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
835 );
836
837 assert_eq!(g1.to_cbor_bytes().unwrap(), g2.to_cbor_bytes().unwrap());
838 assert_eq!(
839 g1.content_id_cbor_default().unwrap(),
840 g2.content_id_cbor_default().unwrap()
841 );
842 }
843
844 #[test]
845 fn test_cbor_more_compact_than_json() {
846 let g = GlobalType::comm(
848 "A",
849 "B",
850 vec![
851 (Label::new("msg1"), GlobalType::End),
852 (Label::new("msg2"), GlobalType::End),
853 (Label::new("msg3"), GlobalType::End),
854 ],
855 );
856
857 let json_bytes = g.to_bytes().unwrap();
858 let cbor_bytes = g.to_cbor_bytes().unwrap();
859
860 assert!(
862 cbor_bytes.len() < json_bytes.len(),
863 "CBOR ({} bytes) should be smaller than JSON ({} bytes)",
864 cbor_bytes.len(),
865 json_bytes.len()
866 );
867 }
868
869 #[test]
870 fn test_json_and_cbor_produce_different_bytes() {
871 let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
873
874 let json_bytes = g.to_bytes().unwrap();
875 let cbor_bytes = g.to_cbor_bytes().unwrap();
876
877 assert_ne!(json_bytes, cbor_bytes);
878 }
879 }
880}
881
882#[cfg(test)]
887mod proptests {
888 use super::*;
889 use proptest::prelude::*;
890
891 fn arb_var_name() -> impl Strategy<Value = String> {
893 prop_oneof![
894 Just("x".to_string()),
895 Just("y".to_string()),
896 Just("z".to_string()),
897 Just("t".to_string()),
898 Just("s".to_string()),
899 ]
900 }
901
902 fn arb_role() -> impl Strategy<Value = String> {
904 prop_oneof![
905 Just("A".to_string()),
906 Just("B".to_string()),
907 Just("C".to_string()),
908 ]
909 }
910
911 fn arb_label() -> impl Strategy<Value = Label> {
913 prop_oneof![
914 Just(Label::new("msg")),
915 Just(Label::new("data")),
916 Just(Label::new("ack")),
917 Just(Label::with_sort("value", PayloadSort::Nat)),
918 Just(Label::with_sort("flag", PayloadSort::Bool)),
919 ]
920 }
921
922 #[allow(dead_code)]
924 fn arb_local_type(depth: usize) -> impl Strategy<Value = LocalTypeR> {
925 if depth == 0 {
926 prop_oneof![
927 Just(LocalTypeR::End),
928 arb_var_name().prop_map(LocalTypeR::var),
929 ]
930 .boxed()
931 } else {
932 prop_oneof![
933 Just(LocalTypeR::End),
934 (arb_role(), arb_label(), arb_local_type(depth - 1))
936 .prop_map(|(partner, label, cont)| LocalTypeR::send(partner, label, cont)),
937 (arb_role(), arb_label(), arb_local_type(depth - 1))
939 .prop_map(|(partner, label, cont)| LocalTypeR::recv(partner, label, cont)),
940 (arb_var_name(), arb_local_type(depth - 1))
942 .prop_map(|(var, body)| LocalTypeR::mu(var, body)),
943 arb_var_name().prop_map(LocalTypeR::var),
945 ]
946 .boxed()
947 }
948 }
949
950 fn rename_global_type(g: &GlobalType, mapping: &[(&str, &str)]) -> GlobalType {
952 fn rename_inner(
953 g: &GlobalType,
954 mapping: &[(&str, &str)],
955 bound: &mut Vec<(String, String)>,
956 ) -> GlobalType {
957 match g {
958 GlobalType::End => GlobalType::End,
959 GlobalType::Comm {
960 sender,
961 receiver,
962 branches,
963 } => GlobalType::Comm {
964 sender: sender.clone(),
965 receiver: receiver.clone(),
966 branches: branches
967 .iter()
968 .map(|(l, cont)| (l.clone(), rename_inner(cont, mapping, bound)))
969 .collect(),
970 },
971 GlobalType::Mu { var, body } => {
972 let new_var = mapping
974 .iter()
975 .find(|(old, _)| *old == var)
976 .map(|(_, new)| (*new).to_string())
977 .unwrap_or_else(|| var.clone());
978
979 bound.push((var.clone(), new_var.clone()));
980 let new_body = rename_inner(body, mapping, bound);
981 bound.pop();
982
983 GlobalType::Mu {
984 var: new_var,
985 body: Box::new(new_body),
986 }
987 }
988 GlobalType::Var(name) => {
989 let new_name = bound
991 .iter()
992 .rev()
993 .find(|(old, _)| old == name)
994 .map(|(_, new)| new.clone())
995 .unwrap_or_else(|| name.clone());
996 GlobalType::Var(new_name)
997 }
998 }
999 }
1000 rename_inner(g, mapping, &mut vec![])
1001 }
1002
1003 fn arb_closed_global_type(depth: usize) -> impl Strategy<Value = GlobalType> {
1006 arb_var_name().prop_flat_map(move |var| {
1008 let var_clone = var.clone();
1009 arb_global_type_closed_body(depth, var)
1010 .prop_map(move |body| GlobalType::mu(var_clone.clone(), body))
1011 })
1012 }
1013
1014 fn arb_global_type_closed_body(
1016 depth: usize,
1017 bound_var: String,
1018 ) -> impl Strategy<Value = GlobalType> {
1019 if depth == 0 {
1020 prop_oneof![
1021 Just(GlobalType::End),
1022 Just(GlobalType::var(bound_var)), ]
1024 .boxed()
1025 } else {
1026 let bv = bound_var.clone();
1027 let bv2 = bound_var.clone();
1028 prop_oneof![
1029 Just(GlobalType::End),
1030 Just(GlobalType::var(bv)),
1031 (arb_role(), arb_role(), arb_label()).prop_flat_map(
1033 move |(sender, receiver, label)| {
1034 let bv_inner = bv2.clone();
1035 arb_global_type_closed_body(depth - 1, bv_inner).prop_map(move |cont| {
1036 GlobalType::send(sender.clone(), receiver.clone(), label.clone(), cont)
1037 })
1038 }
1039 ),
1040 ]
1041 .boxed()
1042 }
1043 }
1044
1045 proptest! {
1046 #[test]
1048 fn prop_content_id_deterministic(g in arb_closed_global_type(3)) {
1049 let cid1 = g.content_id_default().unwrap();
1050 let cid2 = g.content_id_default().unwrap();
1051 prop_assert_eq!(cid1, cid2);
1052 }
1053
1054 #[test]
1056 fn prop_to_bytes_deterministic(g in arb_closed_global_type(3)) {
1057 let bytes1 = g.to_bytes().unwrap();
1058 let bytes2 = g.to_bytes().unwrap();
1059 prop_assert_eq!(bytes1, bytes2);
1060 }
1061
1062 #[test]
1065 fn prop_alpha_equivalence_closed(g in arb_closed_global_type(3)) {
1066 let renamed = rename_global_type(&g, &[("x", "renamed_x"), ("y", "renamed_y"), ("t", "renamed_t")]);
1068
1069 prop_assert_eq!(
1071 g.content_id_default().unwrap(),
1072 renamed.content_id_default().unwrap(),
1073 "α-equivalent closed types should have same content ID"
1074 );
1075 }
1076
1077 #[test]
1079 fn prop_roundtrip_closed(g in arb_closed_global_type(3)) {
1080 let bytes = g.to_bytes().unwrap();
1081 if let Ok(recovered) = GlobalType::from_bytes(&bytes) {
1082 prop_assert_eq!(
1084 g.content_id_default().unwrap(),
1085 recovered.content_id_default().unwrap(),
1086 "roundtrip should preserve content ID for closed types"
1087 );
1088 }
1089 }
1090
1091 #[test]
1093 fn prop_branch_order_invariant(
1094 sender in arb_role(),
1095 receiver in arb_role(),
1096 label1 in arb_label(),
1097 label2 in arb_label(),
1098 ) {
1099 let g1 = GlobalType::comm(
1101 &sender, &receiver,
1102 vec![
1103 (label1.clone(), GlobalType::End),
1104 (label2.clone(), GlobalType::End),
1105 ],
1106 );
1107 let g2 = GlobalType::comm(
1108 &sender, &receiver,
1109 vec![
1110 (label2, GlobalType::End),
1111 (label1, GlobalType::End),
1112 ],
1113 );
1114
1115 prop_assert_eq!(
1117 g1.content_id_default().unwrap(),
1118 g2.content_id_default().unwrap(),
1119 "branch order should not affect content ID"
1120 );
1121 }
1122
1123 #[test]
1125 fn prop_local_type_alpha_equiv(
1126 partner in arb_role(),
1127 label in arb_label(),
1128 ) {
1129 let t1 = LocalTypeR::mu("x", LocalTypeR::send(&partner, label.clone(), LocalTypeR::var("x")));
1130 let t2 = LocalTypeR::mu("y", LocalTypeR::send(&partner, label, LocalTypeR::var("y")));
1131
1132 prop_assert_eq!(
1133 t1.content_id_default().unwrap(),
1134 t2.content_id_default().unwrap(),
1135 "α-equivalent local types should have same content ID"
1136 );
1137 }
1138 }
1139}