1use crate::content_id::{ContentId, Hasher, Sha256Hasher};
28use crate::de_bruijn::{GlobalTypeDB, LocalTypeRDB};
29use crate::{GlobalType, Label, LocalTypeR, PayloadSort};
30use serde::{de::DeserializeOwned, Serialize};
31
32pub trait Contentable: Sized {
59 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError>;
65
66 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
72
73 fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
82 self.to_bytes()
83 }
84
85 #[cfg(feature = "dag-cbor")]
94 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError>;
95
96 #[cfg(feature = "dag-cbor")]
102 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
103
104 fn content_id<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
110 let bytes = self.to_bytes()?;
111 Ok(ContentId::from_bytes(&bytes))
112 }
113
114 fn content_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
120 self.content_id()
121 }
122
123 fn template_id<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
129 let bytes = self.to_template_bytes()?;
130 Ok(ContentId::from_bytes(&bytes))
131 }
132
133 fn template_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
139 self.template_id()
140 }
141
142 #[cfg(feature = "dag-cbor")]
151 fn content_id_cbor<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
152 let bytes = self.to_cbor_bytes()?;
153 Ok(ContentId::from_bytes(&bytes))
154 }
155
156 #[cfg(feature = "dag-cbor")]
162 fn content_id_cbor_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
163 self.content_id_cbor()
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
169pub enum ContentableError {
170 DeserializationFailed(String),
172 SerializationFailed(String),
174 InvalidFormat(String),
176}
177
178impl std::fmt::Display for ContentableError {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 match self {
181 ContentableError::DeserializationFailed(msg) => {
182 write!(f, "deserialization failed: {msg}")
183 }
184 ContentableError::SerializationFailed(msg) => {
185 write!(f, "serialization failed: {msg}")
186 }
187 ContentableError::InvalidFormat(msg) => {
188 write!(f, "invalid format: {msg}")
189 }
190 }
191 }
192}
193
194impl std::error::Error for ContentableError {}
195
196fn to_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
198 serde_json::to_vec(value).map_err(|e| ContentableError::SerializationFailed(e.to_string()))
200}
201
202fn from_json_bytes<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
203 serde_json::from_slice(bytes)
204 .map_err(|e| ContentableError::DeserializationFailed(e.to_string()))
205}
206
207fn sorted_free_vars(mut vars: Vec<String>) -> Vec<String> {
208 vars.sort();
209 vars.dedup();
210 vars
211}
212
213#[derive(Serialize)]
214struct GlobalTemplateEnvelope {
215 free_vars: Vec<String>,
216 db: GlobalTypeDB,
217}
218
219#[derive(Serialize)]
220struct LocalTemplateEnvelope {
221 free_vars: Vec<String>,
222 db: LocalTypeRDB,
223}
224
225#[cfg(feature = "dag-cbor")]
227fn to_cbor_bytes_impl<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
228 serde_ipld_dagcbor::to_vec(value)
229 .map_err(|e| ContentableError::SerializationFailed(format!("dag-cbor: {e}")))
230}
231
232#[cfg(feature = "dag-cbor")]
233fn from_cbor_bytes_impl<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
234 serde_ipld_dagcbor::from_slice(bytes)
235 .map_err(|e| ContentableError::DeserializationFailed(format!("dag-cbor: {e}")))
236}
237
238impl Contentable for PayloadSort {
243 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
244 to_json_bytes(self)
245 }
246
247 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
248 from_json_bytes(bytes)
249 }
250
251 #[cfg(feature = "dag-cbor")]
252 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
253 to_cbor_bytes_impl(self)
254 }
255
256 #[cfg(feature = "dag-cbor")]
257 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
258 from_cbor_bytes_impl(bytes)
259 }
260}
261
262impl Contentable for Label {
263 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
264 to_json_bytes(self)
265 }
266
267 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
268 from_json_bytes(bytes)
269 }
270
271 #[cfg(feature = "dag-cbor")]
272 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
273 to_cbor_bytes_impl(self)
274 }
275
276 #[cfg(feature = "dag-cbor")]
277 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
278 from_cbor_bytes_impl(bytes)
279 }
280}
281
282impl Contentable for GlobalType {
283 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
284 if !self.all_vars_bound() {
285 return Err(ContentableError::InvalidFormat(
286 "canonical serialization requires all recursion variables to be bound".to_string(),
287 ));
288 }
289 let db = GlobalTypeDB::from(self).normalize();
291 to_json_bytes(&db)
292 }
293
294 fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
295 let free_vars = sorted_free_vars(self.free_vars());
296 let env: Vec<&str> = free_vars.iter().map(String::as_str).collect();
297 let db = GlobalTypeDB::from_global_type_with_env(self, &env).normalize();
298 let envelope = GlobalTemplateEnvelope { free_vars, db };
299 to_json_bytes(&envelope)
300 }
301
302 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
303 let db: GlobalTypeDB = from_json_bytes(bytes)?;
306 Ok(global_from_de_bruijn(&db, &mut vec![]))
307 }
308
309 #[cfg(feature = "dag-cbor")]
310 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
311 if !self.all_vars_bound() {
312 return Err(ContentableError::InvalidFormat(
313 "canonical serialization requires all recursion variables to be bound".to_string(),
314 ));
315 }
316 let db = GlobalTypeDB::from(self).normalize();
317 to_cbor_bytes_impl(&db)
318 }
319
320 #[cfg(feature = "dag-cbor")]
321 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
322 let db: GlobalTypeDB = from_cbor_bytes_impl(bytes)?;
323 Ok(global_from_de_bruijn(&db, &mut vec![]))
324 }
325}
326
327impl Contentable for LocalTypeR {
328 fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
329 if !self.all_vars_bound() {
330 return Err(ContentableError::InvalidFormat(
331 "canonical serialization requires all recursion variables to be bound".to_string(),
332 ));
333 }
334 let db = LocalTypeRDB::from(self).normalize();
337 to_json_bytes(&db)
338 }
339
340 fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
341 let free_vars = sorted_free_vars(self.free_vars());
342 let env: Vec<&str> = free_vars.iter().map(String::as_str).collect();
343 let db = LocalTypeRDB::from_local_type_with_env(self, &env).normalize();
344 let envelope = LocalTemplateEnvelope { free_vars, db };
345 to_json_bytes(&envelope)
346 }
347
348 fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
349 let db: LocalTypeRDB = from_json_bytes(bytes)?;
352 Ok(local_from_de_bruijn(&db, &mut vec![]))
353 }
354
355 #[cfg(feature = "dag-cbor")]
356 fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
357 if !self.all_vars_bound() {
358 return Err(ContentableError::InvalidFormat(
359 "canonical serialization requires all recursion variables to be bound".to_string(),
360 ));
361 }
362 let db = LocalTypeRDB::from(self).normalize();
363 to_cbor_bytes_impl(&db)
364 }
365
366 #[cfg(feature = "dag-cbor")]
367 fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
368 let db: LocalTypeRDB = from_cbor_bytes_impl(bytes)?;
369 Ok(local_from_de_bruijn(&db, &mut vec![]))
370 }
371}
372
373fn global_from_de_bruijn(db: &GlobalTypeDB, names: &mut Vec<String>) -> GlobalType {
378 match db {
379 GlobalTypeDB::End => GlobalType::End,
380 GlobalTypeDB::Comm {
381 sender,
382 receiver,
383 branches,
384 } => GlobalType::Comm {
385 sender: sender.clone(),
386 receiver: receiver.clone(),
387 branches: branches
388 .iter()
389 .map(|(l, cont)| (l.clone(), global_from_de_bruijn(cont, names)))
390 .collect(),
391 },
392 GlobalTypeDB::Rec(body) => {
393 let var_name = format!("t{}", names.len());
395 names.push(var_name.clone());
396 let body_converted = global_from_de_bruijn(body, names);
397 names.pop();
398 GlobalType::Mu {
399 var: var_name,
400 body: Box::new(body_converted),
401 }
402 }
403 GlobalTypeDB::Var(idx) => {
404 let name = names
406 .get(names.len().saturating_sub(1 + idx))
407 .cloned()
408 .unwrap_or_else(|| format!("free{idx}"));
409 GlobalType::Var(name)
410 }
411 }
412}
413
414fn local_from_de_bruijn(db: &LocalTypeRDB, names: &mut Vec<String>) -> LocalTypeR {
415 match db {
416 LocalTypeRDB::End => LocalTypeR::End,
417 LocalTypeRDB::Send { partner, branches } => LocalTypeR::Send {
418 partner: partner.clone(),
419 branches: branches
420 .iter()
421 .map(|(l, vt, cont)| (l.clone(), vt.clone(), local_from_de_bruijn(cont, names)))
422 .collect(),
423 },
424 LocalTypeRDB::Recv { partner, branches } => LocalTypeR::Recv {
425 partner: partner.clone(),
426 branches: branches
427 .iter()
428 .map(|(l, vt, cont)| (l.clone(), vt.clone(), local_from_de_bruijn(cont, names)))
429 .collect(),
430 },
431 LocalTypeRDB::Rec(body) => {
432 let var_name = format!("t{}", names.len());
434 names.push(var_name.clone());
435 let body_converted = local_from_de_bruijn(body, names);
436 names.pop();
437 LocalTypeR::Mu {
438 var: var_name,
439 body: Box::new(body_converted),
440 }
441 }
442 LocalTypeRDB::Var(idx) => {
443 let name = names
445 .get(names.len().saturating_sub(1 + idx))
446 .cloned()
447 .unwrap_or_else(|| format!("free{idx}"));
448 LocalTypeR::Var(name)
449 }
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_payload_sort_roundtrip() {
459 let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
460 let bytes = sort.to_bytes().unwrap();
461 let recovered = PayloadSort::from_bytes(&bytes).unwrap();
462 assert_eq!(sort, recovered);
463 }
464
465 #[test]
466 fn test_label_roundtrip() {
467 let label = Label::with_sort("data", PayloadSort::Nat);
468 let bytes = label.to_bytes().unwrap();
469 let recovered = Label::from_bytes(&bytes).unwrap();
470 assert_eq!(label, recovered);
471 }
472
473 #[test]
474 fn test_global_type_alpha_equivalence() {
475 let g1 = GlobalType::mu(
477 "x",
478 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
479 );
480 let g2 = GlobalType::mu(
482 "y",
483 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
484 );
485
486 assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
488
489 assert_eq!(
491 g1.content_id_sha256().unwrap(),
492 g2.content_id_sha256().unwrap()
493 );
494 }
495
496 #[test]
497 fn test_local_type_alpha_equivalence() {
498 let t1 = LocalTypeR::mu(
500 "x",
501 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
502 );
503 let t2 = LocalTypeR::mu(
505 "y",
506 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("y")),
507 );
508
509 assert_eq!(t1.to_bytes().unwrap(), t2.to_bytes().unwrap());
510 assert_eq!(
511 t1.content_id_sha256().unwrap(),
512 t2.content_id_sha256().unwrap()
513 );
514 }
515
516 #[test]
517 fn test_global_type_roundtrip() {
518 let g = GlobalType::mu(
519 "x",
520 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
521 );
522
523 let bytes = g.to_bytes().unwrap();
524 let recovered = GlobalType::from_bytes(&bytes).unwrap();
525
526 assert_eq!(g.to_bytes().unwrap(), recovered.to_bytes().unwrap());
528 }
529
530 #[test]
531 fn test_local_type_roundtrip() {
532 let t = LocalTypeR::mu(
533 "x",
534 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
535 );
536
537 let bytes = t.to_bytes().unwrap();
538 let recovered = LocalTypeR::from_bytes(&bytes).unwrap();
539
540 assert_eq!(t.to_bytes().unwrap(), recovered.to_bytes().unwrap());
541 }
542
543 #[test]
544 fn test_local_type_roundtrip_preserves_payload_annotation() {
545 let t = LocalTypeR::Send {
546 partner: "B".to_string(),
547 branches: vec![(
548 Label::new("msg"),
549 Some(crate::ValType::Nat),
550 LocalTypeR::Recv {
551 partner: "A".to_string(),
552 branches: vec![(
553 Label::new("ack"),
554 Some(crate::ValType::Bool),
555 LocalTypeR::End,
556 )],
557 },
558 )],
559 };
560
561 let bytes = t.to_bytes().unwrap();
562 let recovered = LocalTypeR::from_bytes(&bytes).unwrap();
563 assert_eq!(t, recovered);
564 }
565
566 #[test]
567 fn test_branch_ordering_normalized() {
568 let g1 = GlobalType::comm(
570 "A",
571 "B",
572 vec![
573 (Label::new("b"), GlobalType::End),
574 (Label::new("a"), GlobalType::End),
575 ],
576 );
577 let g2 = GlobalType::comm(
578 "A",
579 "B",
580 vec![
581 (Label::new("a"), GlobalType::End),
582 (Label::new("b"), GlobalType::End),
583 ],
584 );
585
586 assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
587 }
588
589 #[test]
590 fn test_different_types_different_bytes() {
591 let g1 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
592 let g2 = GlobalType::send("A", "B", Label::new("other"), GlobalType::End);
593
594 assert_ne!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
595 assert_ne!(
596 g1.content_id_sha256().unwrap(),
597 g2.content_id_sha256().unwrap()
598 );
599 }
600
601 #[test]
602 fn test_nested_recursion_content_id() {
603 let g1 = GlobalType::mu(
605 "x",
606 GlobalType::mu(
607 "y",
608 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
609 ),
610 );
611 let g2 = GlobalType::mu(
613 "a",
614 GlobalType::mu(
615 "b",
616 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("b")),
617 ),
618 );
619
620 assert_eq!(
621 g1.content_id_sha256().unwrap(),
622 g2.content_id_sha256().unwrap()
623 );
624 }
625
626 #[test]
627 fn test_different_binder_reference() {
628 let g1 = GlobalType::mu(
630 "x",
631 GlobalType::mu(
632 "y",
633 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
634 ),
635 );
636 let g2 = GlobalType::mu(
638 "x",
639 GlobalType::mu(
640 "y",
641 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
642 ),
643 );
644
645 assert_ne!(
647 g1.content_id_sha256().unwrap(),
648 g2.content_id_sha256().unwrap()
649 );
650 }
651
652 #[test]
653 fn test_global_type_open_term_rejected_for_canonical_serialization() {
654 let open = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("free_t"));
655 let err = open.to_bytes().expect_err("open terms must be rejected");
656 assert!(matches!(err, ContentableError::InvalidFormat(_)));
657 }
658
659 #[test]
660 fn test_local_type_open_term_rejected_for_canonical_serialization() {
661 let open = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("free_t"));
662 let err = open.to_bytes().expect_err("open terms must be rejected");
663 assert!(matches!(err, ContentableError::InvalidFormat(_)));
664 }
665
666 #[test]
667 fn test_global_type_open_term_has_template_id() {
668 let open = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("free_t"));
669 let tid = open
670 .template_id_sha256()
671 .expect("open terms should support template IDs");
672 let tid2 = open
673 .template_id_sha256()
674 .expect("template IDs should be deterministic");
675 assert_eq!(tid, tid2);
676 }
677
678 #[test]
679 fn test_local_type_open_term_has_template_id() {
680 let open = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("free_t"));
681 let tid = open
682 .template_id_sha256()
683 .expect("open terms should support template IDs");
684 let tid2 = open
685 .template_id_sha256()
686 .expect("template IDs should be deterministic");
687 assert_eq!(tid, tid2);
688 }
689
690 #[test]
691 fn test_template_id_distinguishes_free_variable_interfaces() {
692 let g1 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x"));
693 let g2 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y"));
694 assert_ne!(
695 g1.template_id_sha256().unwrap(),
696 g2.template_id_sha256().unwrap()
697 );
698 }
699
700 #[cfg(feature = "dag-cbor")]
705 mod cbor_tests {
706 use super::*;
707
708 #[test]
709 fn test_payload_sort_cbor_roundtrip() {
710 let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
711 let bytes = sort.to_cbor_bytes().unwrap();
712 let recovered = PayloadSort::from_cbor_bytes(&bytes).unwrap();
713 assert_eq!(sort, recovered);
714 }
715
716 #[test]
717 fn test_label_cbor_roundtrip() {
718 let label = Label::with_sort("data", PayloadSort::Nat);
719 let bytes = label.to_cbor_bytes().unwrap();
720 let recovered = Label::from_cbor_bytes(&bytes).unwrap();
721 assert_eq!(label, recovered);
722 }
723
724 #[test]
725 fn test_global_type_cbor_roundtrip() {
726 let g = GlobalType::mu(
727 "x",
728 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
729 );
730
731 let bytes = g.to_cbor_bytes().unwrap();
732 let recovered = GlobalType::from_cbor_bytes(&bytes).unwrap();
733
734 assert_eq!(
736 g.to_cbor_bytes().unwrap(),
737 recovered.to_cbor_bytes().unwrap()
738 );
739 }
740
741 #[test]
742 fn test_local_type_cbor_roundtrip() {
743 let t = LocalTypeR::mu(
744 "x",
745 LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
746 );
747
748 let bytes = t.to_cbor_bytes().unwrap();
749 let recovered = LocalTypeR::from_cbor_bytes(&bytes).unwrap();
750
751 assert_eq!(
752 t.to_cbor_bytes().unwrap(),
753 recovered.to_cbor_bytes().unwrap()
754 );
755 }
756
757 #[test]
758 fn test_cbor_alpha_equivalence() {
759 let g1 = GlobalType::mu(
761 "x",
762 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
763 );
764 let g2 = GlobalType::mu(
765 "y",
766 GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
767 );
768
769 assert_eq!(g1.to_cbor_bytes().unwrap(), g2.to_cbor_bytes().unwrap());
770 assert_eq!(
771 g1.content_id_cbor_sha256().unwrap(),
772 g2.content_id_cbor_sha256().unwrap()
773 );
774 }
775
776 #[test]
777 fn test_cbor_more_compact_than_json() {
778 let g = GlobalType::comm(
780 "A",
781 "B",
782 vec![
783 (Label::new("msg1"), GlobalType::End),
784 (Label::new("msg2"), GlobalType::End),
785 (Label::new("msg3"), GlobalType::End),
786 ],
787 );
788
789 let json_bytes = g.to_bytes().unwrap();
790 let cbor_bytes = g.to_cbor_bytes().unwrap();
791
792 assert!(
794 cbor_bytes.len() < json_bytes.len(),
795 "CBOR ({} bytes) should be smaller than JSON ({} bytes)",
796 cbor_bytes.len(),
797 json_bytes.len()
798 );
799 }
800
801 #[test]
802 fn test_json_and_cbor_produce_different_bytes() {
803 let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
805
806 let json_bytes = g.to_bytes().unwrap();
807 let cbor_bytes = g.to_cbor_bytes().unwrap();
808
809 assert_ne!(json_bytes, cbor_bytes);
810 }
811 }
812}
813
814#[cfg(test)]
819mod proptests {
820 use super::*;
821 use proptest::prelude::*;
822
823 fn arb_var_name() -> impl Strategy<Value = String> {
825 prop_oneof![
826 Just("x".to_string()),
827 Just("y".to_string()),
828 Just("z".to_string()),
829 Just("t".to_string()),
830 Just("s".to_string()),
831 ]
832 }
833
834 fn arb_role() -> impl Strategy<Value = String> {
836 prop_oneof![
837 Just("A".to_string()),
838 Just("B".to_string()),
839 Just("C".to_string()),
840 ]
841 }
842
843 fn arb_label() -> impl Strategy<Value = Label> {
845 prop_oneof![
846 Just(Label::new("msg")),
847 Just(Label::new("data")),
848 Just(Label::new("ack")),
849 Just(Label::with_sort("value", PayloadSort::Nat)),
850 Just(Label::with_sort("flag", PayloadSort::Bool)),
851 ]
852 }
853
854 #[allow(dead_code)]
856 fn arb_local_type(depth: usize) -> impl Strategy<Value = LocalTypeR> {
857 if depth == 0 {
858 prop_oneof![
859 Just(LocalTypeR::End),
860 arb_var_name().prop_map(LocalTypeR::var),
861 ]
862 .boxed()
863 } else {
864 prop_oneof![
865 Just(LocalTypeR::End),
866 (arb_role(), arb_label(), arb_local_type(depth - 1))
868 .prop_map(|(partner, label, cont)| LocalTypeR::send(partner, label, cont)),
869 (arb_role(), arb_label(), arb_local_type(depth - 1))
871 .prop_map(|(partner, label, cont)| LocalTypeR::recv(partner, label, cont)),
872 (arb_var_name(), arb_local_type(depth - 1))
874 .prop_map(|(var, body)| LocalTypeR::mu(var, body)),
875 arb_var_name().prop_map(LocalTypeR::var),
877 ]
878 .boxed()
879 }
880 }
881
882 fn rename_global_type(g: &GlobalType, mapping: &[(&str, &str)]) -> GlobalType {
884 fn rename_inner(
885 g: &GlobalType,
886 mapping: &[(&str, &str)],
887 bound: &mut Vec<(String, String)>,
888 ) -> GlobalType {
889 match g {
890 GlobalType::End => GlobalType::End,
891 GlobalType::Comm {
892 sender,
893 receiver,
894 branches,
895 } => GlobalType::Comm {
896 sender: sender.clone(),
897 receiver: receiver.clone(),
898 branches: branches
899 .iter()
900 .map(|(l, cont)| (l.clone(), rename_inner(cont, mapping, bound)))
901 .collect(),
902 },
903 GlobalType::Mu { var, body } => {
904 let new_var = mapping
906 .iter()
907 .find(|(old, _)| *old == var)
908 .map(|(_, new)| (*new).to_string())
909 .unwrap_or_else(|| var.clone());
910
911 bound.push((var.clone(), new_var.clone()));
912 let new_body = rename_inner(body, mapping, bound);
913 bound.pop();
914
915 GlobalType::Mu {
916 var: new_var,
917 body: Box::new(new_body),
918 }
919 }
920 GlobalType::Var(name) => {
921 let new_name = bound
923 .iter()
924 .rev()
925 .find(|(old, _)| old == name)
926 .map(|(_, new)| new.clone())
927 .unwrap_or_else(|| name.clone());
928 GlobalType::Var(new_name)
929 }
930 }
931 }
932 rename_inner(g, mapping, &mut vec![])
933 }
934
935 fn arb_closed_global_type(depth: usize) -> impl Strategy<Value = GlobalType> {
938 arb_var_name().prop_flat_map(move |var| {
940 let var_clone = var.clone();
941 arb_global_type_closed_body(depth, var)
942 .prop_map(move |body| GlobalType::mu(var_clone.clone(), body))
943 })
944 }
945
946 fn arb_global_type_closed_body(
948 depth: usize,
949 bound_var: String,
950 ) -> impl Strategy<Value = GlobalType> {
951 if depth == 0 {
952 prop_oneof![
953 Just(GlobalType::End),
954 Just(GlobalType::var(bound_var)), ]
956 .boxed()
957 } else {
958 let bv = bound_var.clone();
959 let bv2 = bound_var.clone();
960 prop_oneof![
961 Just(GlobalType::End),
962 Just(GlobalType::var(bv)),
963 (arb_role(), arb_role(), arb_label()).prop_flat_map(
965 move |(sender, receiver, label)| {
966 let bv_inner = bv2.clone();
967 arb_global_type_closed_body(depth - 1, bv_inner).prop_map(move |cont| {
968 GlobalType::send(sender.clone(), receiver.clone(), label.clone(), cont)
969 })
970 }
971 ),
972 ]
973 .boxed()
974 }
975 }
976
977 proptest! {
978 #[test]
980 fn prop_content_id_deterministic(g in arb_closed_global_type(3)) {
981 let cid1 = g.content_id_sha256().unwrap();
982 let cid2 = g.content_id_sha256().unwrap();
983 prop_assert_eq!(cid1, cid2);
984 }
985
986 #[test]
988 fn prop_to_bytes_deterministic(g in arb_closed_global_type(3)) {
989 let bytes1 = g.to_bytes().unwrap();
990 let bytes2 = g.to_bytes().unwrap();
991 prop_assert_eq!(bytes1, bytes2);
992 }
993
994 #[test]
997 fn prop_alpha_equivalence_closed(g in arb_closed_global_type(3)) {
998 let renamed = rename_global_type(&g, &[("x", "renamed_x"), ("y", "renamed_y"), ("t", "renamed_t")]);
1000
1001 prop_assert_eq!(
1003 g.content_id_sha256().unwrap(),
1004 renamed.content_id_sha256().unwrap(),
1005 "α-equivalent closed types should have same content ID"
1006 );
1007 }
1008
1009 #[test]
1011 fn prop_roundtrip_closed(g in arb_closed_global_type(3)) {
1012 let bytes = g.to_bytes().unwrap();
1013 if let Ok(recovered) = GlobalType::from_bytes(&bytes) {
1014 prop_assert_eq!(
1016 g.content_id_sha256().unwrap(),
1017 recovered.content_id_sha256().unwrap(),
1018 "roundtrip should preserve content ID for closed types"
1019 );
1020 }
1021 }
1022
1023 #[test]
1025 fn prop_branch_order_invariant(
1026 sender in arb_role(),
1027 receiver in arb_role(),
1028 label1 in arb_label(),
1029 label2 in arb_label(),
1030 ) {
1031 let g1 = GlobalType::comm(
1033 &sender, &receiver,
1034 vec![
1035 (label1.clone(), GlobalType::End),
1036 (label2.clone(), GlobalType::End),
1037 ],
1038 );
1039 let g2 = GlobalType::comm(
1040 &sender, &receiver,
1041 vec![
1042 (label2, GlobalType::End),
1043 (label1, GlobalType::End),
1044 ],
1045 );
1046
1047 prop_assert_eq!(
1049 g1.content_id_sha256().unwrap(),
1050 g2.content_id_sha256().unwrap(),
1051 "branch order should not affect content ID"
1052 );
1053 }
1054
1055 #[test]
1057 fn prop_local_type_alpha_equiv(
1058 partner in arb_role(),
1059 label in arb_label(),
1060 ) {
1061 let t1 = LocalTypeR::mu("x", LocalTypeR::send(&partner, label.clone(), LocalTypeR::var("x")));
1062 let t2 = LocalTypeR::mu("y", LocalTypeR::send(&partner, label, LocalTypeR::var("y")));
1063
1064 prop_assert_eq!(
1065 t1.content_id_sha256().unwrap(),
1066 t2.content_id_sha256().unwrap(),
1067 "α-equivalent local types should have same content ID"
1068 );
1069 }
1070 }
1071}