1use crate::content_id::{ContentId, Hasher, Sha256Hasher};
25use crate::de_bruijn::{GlobalTypeDB, LocalTypeRDB};
26use crate::{GlobalType, Label, LocalTypeR, PayloadSort};
27use serde::{de::DeserializeOwned, Serialize};
28
29pub trait Contentable: Sized {
53 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError>;
55
56 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
62
63 #[cfg(feature = "dag-cbor")]
68 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError>;
69
70 #[cfg(feature = "dag-cbor")]
76 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
77
78 fn content_id<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
80 let bytes = self.to_bytes()?;
81 Ok(ContentId::from_bytes(&bytes))
82 }
83
84 fn content_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
86 self.content_id()
87 }
88
89 #[cfg(feature = "dag-cbor")]
94 fn content_id_cbor<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
95 let bytes = self.to_cbor_bytes()?;
96 Ok(ContentId::from_bytes(&bytes))
97 }
98
99 #[cfg(feature = "dag-cbor")]
101 fn content_id_cbor_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
102 self.content_id_cbor()
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum ContentableError {
109 DeserializationFailed(String),
111 SerializationFailed(String),
113 InvalidFormat(String),
115}
116
117impl std::fmt::Display for ContentableError {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 ContentableError::DeserializationFailed(msg) => {
121 write!(f, "deserialization failed: {msg}")
122 }
123 ContentableError::SerializationFailed(msg) => {
124 write!(f, "serialization failed: {msg}")
125 }
126 ContentableError::InvalidFormat(msg) => {
127 write!(f, "invalid format: {msg}")
128 }
129 }
130 }
131}
132
133impl std::error::Error for ContentableError {}
134
135fn to_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
137 serde_json::to_vec(value).map_err(|e| ContentableError::SerializationFailed(e.to_string()))
139}
140
141fn from_json_bytes<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
142 serde_json::from_slice(bytes)
143 .map_err(|e| ContentableError::DeserializationFailed(e.to_string()))
144}
145
146#[cfg(feature = "dag-cbor")]
148fn to_cbor_bytes_impl<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
149 serde_ipld_dagcbor::to_vec(value)
150 .map_err(|e| ContentableError::SerializationFailed(format!("dag-cbor: {e}")))
151}
152
153#[cfg(feature = "dag-cbor")]
154fn from_cbor_bytes_impl<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
155 serde_ipld_dagcbor::from_slice(bytes)
156 .map_err(|e| ContentableError::DeserializationFailed(format!("dag-cbor: {e}")))
157}
158
159impl Contentable for PayloadSort {
164 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
165 to_json_bytes(self)
166 }
167
168 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
169 from_json_bytes(bytes)
170 }
171
172 #[cfg(feature = "dag-cbor")]
173 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
174 to_cbor_bytes_impl(self)
175 }
176
177 #[cfg(feature = "dag-cbor")]
178 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
179 from_cbor_bytes_impl(bytes)
180 }
181}
182
183impl Contentable for Label {
184 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
185 to_json_bytes(self)
186 }
187
188 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
189 from_json_bytes(bytes)
190 }
191
192 #[cfg(feature = "dag-cbor")]
193 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
194 to_cbor_bytes_impl(self)
195 }
196
197 #[cfg(feature = "dag-cbor")]
198 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
199 from_cbor_bytes_impl(bytes)
200 }
201}
202
203impl Contentable for GlobalType {
204 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
205 let db = GlobalTypeDB::from(self).normalize();
207 to_json_bytes(&db)
208 }
209
210 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
211 let db: GlobalTypeDB = from_json_bytes(bytes)?;
214 Ok(global_from_de_bruijn(&db, &mut vec![]))
215 }
216
217 #[cfg(feature = "dag-cbor")]
218 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
219 let db = GlobalTypeDB::from(self).normalize();
220 to_cbor_bytes_impl(&db)
221 }
222
223 #[cfg(feature = "dag-cbor")]
224 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
225 let db: GlobalTypeDB = from_cbor_bytes_impl(bytes)?;
226 Ok(global_from_de_bruijn(&db, &mut vec![]))
227 }
228}
229
230impl Contentable for LocalTypeR {
231 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
232 let db = LocalTypeRDB::from(self).normalize();
234 to_json_bytes(&db)
235 }
236
237 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
238 let db: LocalTypeRDB = from_json_bytes(bytes)?;
241 Ok(local_from_de_bruijn(&db, &mut vec![]))
242 }
243
244 #[cfg(feature = "dag-cbor")]
245 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
246 let db = LocalTypeRDB::from(self).normalize();
247 to_cbor_bytes_impl(&db)
248 }
249
250 #[cfg(feature = "dag-cbor")]
251 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
252 let db: LocalTypeRDB = from_cbor_bytes_impl(bytes)?;
253 Ok(local_from_de_bruijn(&db, &mut vec![]))
254 }
255}
256
257fn global_from_de_bruijn(db: &GlobalTypeDB, names: &mut Vec<String>) -> GlobalType {
262 match db {
263 GlobalTypeDB::End => GlobalType::End,
264 GlobalTypeDB::Comm {
265 sender,
266 receiver,
267 branches,
268 } => GlobalType::Comm {
269 sender: sender.clone(),
270 receiver: receiver.clone(),
271 branches: branches
272 .iter()
273 .map(|(l, cont)| (l.clone(), global_from_de_bruijn(cont, names)))
274 .collect(),
275 },
276 GlobalTypeDB::Rec(body) => {
277 let var_name = format!("t{}", names.len());
279 names.push(var_name.clone());
280 let body_converted = global_from_de_bruijn(body, names);
281 names.pop();
282 GlobalType::Mu {
283 var: var_name,
284 body: Box::new(body_converted),
285 }
286 }
287 GlobalTypeDB::Var(idx) => {
288 let name = names
290 .get(names.len().saturating_sub(1 + idx))
291 .cloned()
292 .unwrap_or_else(|| format!("free{idx}"));
293 GlobalType::Var(name)
294 }
295 }
296}
297
298fn local_from_de_bruijn(db: &LocalTypeRDB, names: &mut Vec<String>) -> LocalTypeR {
299 match db {
300 LocalTypeRDB::End => LocalTypeR::End,
301 LocalTypeRDB::Send { partner, branches } => LocalTypeR::Send {
302 partner: partner.clone(),
303 branches: branches
304 .iter()
305 .map(|(l, cont)| (l.clone(), local_from_de_bruijn(cont, names)))
306 .collect(),
307 },
308 LocalTypeRDB::Recv { partner, branches } => LocalTypeR::Recv {
309 partner: partner.clone(),
310 branches: branches
311 .iter()
312 .map(|(l, cont)| (l.clone(), local_from_de_bruijn(cont, names)))
313 .collect(),
314 },
315 LocalTypeRDB::Rec(body) => {
316 let var_name = format!("t{}", names.len());
318 names.push(var_name.clone());
319 let body_converted = local_from_de_bruijn(body, names);
320 names.pop();
321 LocalTypeR::Mu {
322 var: var_name,
323 body: Box::new(body_converted),
324 }
325 }
326 LocalTypeRDB::Var(idx) => {
327 let name = names
329 .get(names.len().saturating_sub(1 + idx))
330 .cloned()
331 .unwrap_or_else(|| format!("free{idx}"));
332 LocalTypeR::Var(name)
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn test_payload_sort_roundtrip() {
343 let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
344 let bytes = sort.to_bytes().unwrap();
345 let recovered = PayloadSort::from_bytes(&bytes).unwrap();
346 assert_eq!(sort, recovered);
347 }
348
349 #[test]
350 fn test_label_roundtrip() {
351 let label = Label::with_sort("data", PayloadSort::Nat);
352 let bytes = label.to_bytes().unwrap();
353 let recovered = Label::from_bytes(&bytes).unwrap();
354 assert_eq!(label, recovered);
355 }
356
357 #[test]
358 fn test_global_type_alpha_equivalence() {
359 let g1 = GlobalType::mu(
361 "x",
362 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
363 );
364 let g2 = GlobalType::mu(
366 "y",
367 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
368 );
369
370 assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
372
373 assert_eq!(
375 g1.content_id_sha256().unwrap(),
376 g2.content_id_sha256().unwrap()
377 );
378 }
379
380 #[test]
381 fn test_local_type_alpha_equivalence() {
382 let t1 = LocalTypeR::mu(
384 "x",
385 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
386 );
387 let t2 = LocalTypeR::mu(
389 "y",
390 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("y")),
391 );
392
393 assert_eq!(t1.to_bytes().unwrap(), t2.to_bytes().unwrap());
394 assert_eq!(
395 t1.content_id_sha256().unwrap(),
396 t2.content_id_sha256().unwrap()
397 );
398 }
399
400 #[test]
401 fn test_global_type_roundtrip() {
402 let g = GlobalType::mu(
403 "x",
404 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
405 );
406
407 let bytes = g.to_bytes().unwrap();
408 let recovered = GlobalType::from_bytes(&bytes).unwrap();
409
410 assert_eq!(g.to_bytes().unwrap(), recovered.to_bytes().unwrap());
412 }
413
414 #[test]
415 fn test_local_type_roundtrip() {
416 let t = LocalTypeR::mu(
417 "x",
418 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
419 );
420
421 let bytes = t.to_bytes().unwrap();
422 let recovered = LocalTypeR::from_bytes(&bytes).unwrap();
423
424 assert_eq!(t.to_bytes().unwrap(), recovered.to_bytes().unwrap());
425 }
426
427 #[test]
428 fn test_branch_ordering_normalized() {
429 let g1 = GlobalType::comm(
431 "A",
432 "B",
433 vec![
434 (Label::new("b"), GlobalType::End),
435 (Label::new("a"), GlobalType::End),
436 ],
437 );
438 let g2 = GlobalType::comm(
439 "A",
440 "B",
441 vec![
442 (Label::new("a"), GlobalType::End),
443 (Label::new("b"), GlobalType::End),
444 ],
445 );
446
447 assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
448 }
449
450 #[test]
451 fn test_different_types_different_bytes() {
452 let g1 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
453 let g2 = GlobalType::send("A", "B", Label::new("other"), GlobalType::End);
454
455 assert_ne!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
456 assert_ne!(
457 g1.content_id_sha256().unwrap(),
458 g2.content_id_sha256().unwrap()
459 );
460 }
461
462 #[test]
463 fn test_nested_recursion_content_id() {
464 let g1 = GlobalType::mu(
466 "x",
467 GlobalType::mu(
468 "y",
469 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
470 ),
471 );
472 let g2 = GlobalType::mu(
474 "a",
475 GlobalType::mu(
476 "b",
477 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("b")),
478 ),
479 );
480
481 assert_eq!(
482 g1.content_id_sha256().unwrap(),
483 g2.content_id_sha256().unwrap()
484 );
485 }
486
487 #[test]
488 fn test_different_binder_reference() {
489 let g1 = GlobalType::mu(
491 "x",
492 GlobalType::mu(
493 "y",
494 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
495 ),
496 );
497 let g2 = GlobalType::mu(
499 "x",
500 GlobalType::mu(
501 "y",
502 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
503 ),
504 );
505
506 assert_ne!(
508 g1.content_id_sha256().unwrap(),
509 g2.content_id_sha256().unwrap()
510 );
511 }
512
513 #[cfg(feature = "dag-cbor")]
518 mod cbor_tests {
519 use super::*;
520
521 #[test]
522 fn test_payload_sort_cbor_roundtrip() {
523 let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
524 let bytes = sort.to_cbor_bytes().unwrap();
525 let recovered = PayloadSort::from_cbor_bytes(&bytes).unwrap();
526 assert_eq!(sort, recovered);
527 }
528
529 #[test]
530 fn test_label_cbor_roundtrip() {
531 let label = Label::with_sort("data", PayloadSort::Nat);
532 let bytes = label.to_cbor_bytes().unwrap();
533 let recovered = Label::from_cbor_bytes(&bytes).unwrap();
534 assert_eq!(label, recovered);
535 }
536
537 #[test]
538 fn test_global_type_cbor_roundtrip() {
539 let g = GlobalType::mu(
540 "x",
541 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
542 );
543
544 let bytes = g.to_cbor_bytes().unwrap();
545 let recovered = GlobalType::from_cbor_bytes(&bytes).unwrap();
546
547 assert_eq!(
549 g.to_cbor_bytes().unwrap(),
550 recovered.to_cbor_bytes().unwrap()
551 );
552 }
553
554 #[test]
555 fn test_local_type_cbor_roundtrip() {
556 let t = LocalTypeR::mu(
557 "x",
558 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
559 );
560
561 let bytes = t.to_cbor_bytes().unwrap();
562 let recovered = LocalTypeR::from_cbor_bytes(&bytes).unwrap();
563
564 assert_eq!(
565 t.to_cbor_bytes().unwrap(),
566 recovered.to_cbor_bytes().unwrap()
567 );
568 }
569
570 #[test]
571 fn test_cbor_alpha_equivalence() {
572 let g1 = GlobalType::mu(
574 "x",
575 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
576 );
577 let g2 = GlobalType::mu(
578 "y",
579 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
580 );
581
582 assert_eq!(g1.to_cbor_bytes().unwrap(), g2.to_cbor_bytes().unwrap());
583 assert_eq!(
584 g1.content_id_cbor_sha256().unwrap(),
585 g2.content_id_cbor_sha256().unwrap()
586 );
587 }
588
589 #[test]
590 fn test_cbor_more_compact_than_json() {
591 let g = GlobalType::comm(
593 "A",
594 "B",
595 vec![
596 (Label::new("msg1"), GlobalType::End),
597 (Label::new("msg2"), GlobalType::End),
598 (Label::new("msg3"), GlobalType::End),
599 ],
600 );
601
602 let json_bytes = g.to_bytes().unwrap();
603 let cbor_bytes = g.to_cbor_bytes().unwrap();
604
605 assert!(
607 cbor_bytes.len() < json_bytes.len(),
608 "CBOR ({} bytes) should be smaller than JSON ({} bytes)",
609 cbor_bytes.len(),
610 json_bytes.len()
611 );
612 }
613
614 #[test]
615 fn test_json_and_cbor_produce_different_bytes() {
616 let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
618
619 let json_bytes = g.to_bytes().unwrap();
620 let cbor_bytes = g.to_cbor_bytes().unwrap();
621
622 assert_ne!(json_bytes, cbor_bytes);
623 }
624 }
625}
626
627#[cfg(test)]
632mod proptests {
633 use super::*;
634 use proptest::prelude::*;
635
636 fn arb_var_name() -> impl Strategy<Value = String> {
638 prop_oneof![
639 Just("x".to_string()),
640 Just("y".to_string()),
641 Just("z".to_string()),
642 Just("t".to_string()),
643 Just("s".to_string()),
644 ]
645 }
646
647 fn arb_role() -> impl Strategy<Value = String> {
649 prop_oneof![
650 Just("A".to_string()),
651 Just("B".to_string()),
652 Just("C".to_string()),
653 ]
654 }
655
656 fn arb_label() -> impl Strategy<Value = Label> {
658 prop_oneof![
659 Just(Label::new("msg")),
660 Just(Label::new("data")),
661 Just(Label::new("ack")),
662 Just(Label::with_sort("value", PayloadSort::Nat)),
663 Just(Label::with_sort("flag", PayloadSort::Bool)),
664 ]
665 }
666
667 fn arb_global_type(depth: usize) -> impl Strategy<Value = GlobalType> {
669 if depth == 0 {
670 prop_oneof![
671 Just(GlobalType::End),
672 arb_var_name().prop_map(GlobalType::var),
673 ]
674 .boxed()
675 } else {
676 prop_oneof![
677 Just(GlobalType::End),
678 (
680 arb_role(),
681 arb_role(),
682 arb_label(),
683 arb_global_type(depth - 1)
684 )
685 .prop_map(|(sender, receiver, label, cont)| {
686 GlobalType::send(sender, receiver, label, cont)
687 }),
688 (arb_var_name(), arb_global_type(depth - 1))
690 .prop_map(|(var, body)| GlobalType::mu(var, body)),
691 arb_var_name().prop_map(GlobalType::var),
693 ]
694 .boxed()
695 }
696 }
697
698 #[allow(dead_code)]
700 fn arb_local_type(depth: usize) -> impl Strategy<Value = LocalTypeR> {
701 if depth == 0 {
702 prop_oneof![
703 Just(LocalTypeR::End),
704 arb_var_name().prop_map(LocalTypeR::var),
705 ]
706 .boxed()
707 } else {
708 prop_oneof![
709 Just(LocalTypeR::End),
710 (arb_role(), arb_label(), arb_local_type(depth - 1))
712 .prop_map(|(partner, label, cont)| LocalTypeR::send(partner, label, cont)),
713 (arb_role(), arb_label(), arb_local_type(depth - 1))
715 .prop_map(|(partner, label, cont)| LocalTypeR::recv(partner, label, cont)),
716 (arb_var_name(), arb_local_type(depth - 1))
718 .prop_map(|(var, body)| LocalTypeR::mu(var, body)),
719 arb_var_name().prop_map(LocalTypeR::var),
721 ]
722 .boxed()
723 }
724 }
725
726 fn rename_global_type(g: &GlobalType, mapping: &[(&str, &str)]) -> GlobalType {
728 fn rename_inner(
729 g: &GlobalType,
730 mapping: &[(&str, &str)],
731 bound: &mut Vec<(String, String)>,
732 ) -> GlobalType {
733 match g {
734 GlobalType::End => GlobalType::End,
735 GlobalType::Comm {
736 sender,
737 receiver,
738 branches,
739 } => GlobalType::Comm {
740 sender: sender.clone(),
741 receiver: receiver.clone(),
742 branches: branches
743 .iter()
744 .map(|(l, cont)| (l.clone(), rename_inner(cont, mapping, bound)))
745 .collect(),
746 },
747 GlobalType::Mu { var, body } => {
748 let new_var = mapping
750 .iter()
751 .find(|(old, _)| *old == var)
752 .map(|(_, new)| new.to_string())
753 .unwrap_or_else(|| var.clone());
754
755 bound.push((var.clone(), new_var.clone()));
756 let new_body = rename_inner(body, mapping, bound);
757 bound.pop();
758
759 GlobalType::Mu {
760 var: new_var,
761 body: Box::new(new_body),
762 }
763 }
764 GlobalType::Var(name) => {
765 let new_name = bound
767 .iter()
768 .rev()
769 .find(|(old, _)| old == name)
770 .map(|(_, new)| new.clone())
771 .unwrap_or_else(|| name.clone());
772 GlobalType::Var(new_name)
773 }
774 }
775 }
776 rename_inner(g, mapping, &mut vec![])
777 }
778
779 fn arb_closed_global_type(depth: usize) -> impl Strategy<Value = GlobalType> {
782 arb_var_name().prop_flat_map(move |var| {
784 let var_clone = var.clone();
785 arb_global_type_closed_body(depth, var)
786 .prop_map(move |body| GlobalType::mu(var_clone.clone(), body))
787 })
788 }
789
790 fn arb_global_type_closed_body(
792 depth: usize,
793 bound_var: String,
794 ) -> impl Strategy<Value = GlobalType> {
795 if depth == 0 {
796 prop_oneof![
797 Just(GlobalType::End),
798 Just(GlobalType::var(bound_var)), ]
800 .boxed()
801 } else {
802 let bv = bound_var.clone();
803 let bv2 = bound_var.clone();
804 prop_oneof![
805 Just(GlobalType::End),
806 Just(GlobalType::var(bv)),
807 (arb_role(), arb_role(), arb_label()).prop_flat_map(
809 move |(sender, receiver, label)| {
810 let bv_inner = bv2.clone();
811 arb_global_type_closed_body(depth - 1, bv_inner).prop_map(move |cont| {
812 GlobalType::send(sender.clone(), receiver.clone(), label.clone(), cont)
813 })
814 }
815 ),
816 ]
817 .boxed()
818 }
819 }
820
821 proptest! {
822 #[test]
824 fn prop_content_id_deterministic(g in arb_global_type(3)) {
825 let cid1 = g.content_id_sha256().unwrap();
826 let cid2 = g.content_id_sha256().unwrap();
827 prop_assert_eq!(cid1, cid2);
828 }
829
830 #[test]
832 fn prop_to_bytes_deterministic(g in arb_global_type(3)) {
833 let bytes1 = g.to_bytes().unwrap();
834 let bytes2 = g.to_bytes().unwrap();
835 prop_assert_eq!(bytes1, bytes2);
836 }
837
838 #[test]
841 fn prop_alpha_equivalence_closed(g in arb_closed_global_type(3)) {
842 let renamed = rename_global_type(&g, &[("x", "renamed_x"), ("y", "renamed_y"), ("t", "renamed_t")]);
844
845 prop_assert_eq!(
847 g.content_id_sha256().unwrap(),
848 renamed.content_id_sha256().unwrap(),
849 "α-equivalent closed types should have same content ID"
850 );
851 }
852
853 #[test]
855 fn prop_roundtrip_closed(g in arb_closed_global_type(3)) {
856 let bytes = g.to_bytes().unwrap();
857 if let Ok(recovered) = GlobalType::from_bytes(&bytes) {
858 prop_assert_eq!(
860 g.content_id_sha256().unwrap(),
861 recovered.content_id_sha256().unwrap(),
862 "roundtrip should preserve content ID for closed types"
863 );
864 }
865 }
866
867 #[test]
869 fn prop_branch_order_invariant(
870 sender in arb_role(),
871 receiver in arb_role(),
872 label1 in arb_label(),
873 label2 in arb_label(),
874 ) {
875 let g1 = GlobalType::comm(
877 &sender, &receiver,
878 vec![
879 (label1.clone(), GlobalType::End),
880 (label2.clone(), GlobalType::End),
881 ],
882 );
883 let g2 = GlobalType::comm(
884 &sender, &receiver,
885 vec![
886 (label2, GlobalType::End),
887 (label1, GlobalType::End),
888 ],
889 );
890
891 prop_assert_eq!(
893 g1.content_id_sha256().unwrap(),
894 g2.content_id_sha256().unwrap(),
895 "branch order should not affect content ID"
896 );
897 }
898
899 #[test]
901 fn prop_local_type_alpha_equiv(
902 partner in arb_role(),
903 label in arb_label(),
904 ) {
905 let t1 = LocalTypeR::mu("x", LocalTypeR::send(&partner, label.clone(), LocalTypeR::var("x")));
906 let t2 = LocalTypeR::mu("y", LocalTypeR::send(&partner, label, LocalTypeR::var("y")));
907
908 prop_assert_eq!(
909 t1.content_id_sha256().unwrap(),
910 t2.content_id_sha256().unwrap(),
911 "α-equivalent local types should have same content ID"
912 );
913 }
914 }
915}