#[cfg(feature = "sha256")]
use crate::content_id::Sha256Hasher;
use crate::content_id::{Blake3Hasher, ContentId, DefaultContentHasher, Hasher};
use crate::de_bruijn::{GlobalTypeDB, LocalTypeRDB};
use crate::{GlobalType, Label, LocalTypeR, PayloadSort};
use serde::{de::DeserializeOwned, Serialize};
pub trait Contentable: Sized {
fn to_bytes(&self) -> Result<Vec<u8>, ContentableError>;
fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
self.to_bytes()
}
#[cfg(feature = "dag-cbor")]
fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError>;
#[cfg(feature = "dag-cbor")]
fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError>;
fn content_id<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
let bytes = self.to_bytes()?;
Ok(ContentId::from_bytes(&bytes))
}
fn content_id_default(&self) -> Result<ContentId<DefaultContentHasher>, ContentableError> {
self.content_id()
}
fn content_id_blake3(&self) -> Result<ContentId<Blake3Hasher>, ContentableError> {
self.content_id()
}
#[cfg(feature = "sha256")]
fn content_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
self.content_id()
}
fn template_id<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
let bytes = self.to_template_bytes()?;
Ok(ContentId::from_bytes(&bytes))
}
fn template_id_default(&self) -> Result<ContentId<DefaultContentHasher>, ContentableError> {
self.template_id()
}
fn template_id_blake3(&self) -> Result<ContentId<Blake3Hasher>, ContentableError> {
self.template_id()
}
#[cfg(feature = "sha256")]
fn template_id_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
self.template_id()
}
#[cfg(feature = "dag-cbor")]
fn content_id_cbor<H: Hasher>(&self) -> Result<ContentId<H>, ContentableError> {
let bytes = self.to_cbor_bytes()?;
Ok(ContentId::from_bytes(&bytes))
}
#[cfg(feature = "dag-cbor")]
fn content_id_cbor_default(&self) -> Result<ContentId<DefaultContentHasher>, ContentableError> {
self.content_id_cbor()
}
#[cfg(feature = "dag-cbor")]
fn content_id_cbor_blake3(&self) -> Result<ContentId<Blake3Hasher>, ContentableError> {
self.content_id_cbor()
}
#[cfg(all(feature = "dag-cbor", feature = "sha256"))]
fn content_id_cbor_sha256(&self) -> Result<ContentId<Sha256Hasher>, ContentableError> {
self.content_id_cbor()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContentableError {
DeserializationFailed(String),
SerializationFailed(String),
InvalidFormat(String),
}
impl std::fmt::Display for ContentableError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContentableError::DeserializationFailed(msg) => {
write!(f, "deserialization failed: {msg}")
}
ContentableError::SerializationFailed(msg) => {
write!(f, "serialization failed: {msg}")
}
ContentableError::InvalidFormat(msg) => {
write!(f, "invalid format: {msg}")
}
}
}
}
impl std::error::Error for ContentableError {}
fn to_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
serde_json::to_vec(value).map_err(|e| ContentableError::SerializationFailed(e.to_string()))
}
fn from_json_bytes<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
serde_json::from_slice(bytes)
.map_err(|e| ContentableError::DeserializationFailed(e.to_string()))
}
fn sorted_free_vars(mut vars: Vec<String>) -> Vec<String> {
vars.sort();
vars.dedup();
vars
}
#[derive(Serialize)]
struct GlobalTemplateEnvelope {
free_vars: Vec<String>,
db: GlobalTypeDB,
}
#[derive(Serialize)]
struct LocalTemplateEnvelope {
free_vars: Vec<String>,
db: LocalTypeRDB,
}
#[cfg(feature = "dag-cbor")]
fn to_cbor_bytes_impl<T: Serialize>(value: &T) -> Result<Vec<u8>, ContentableError> {
serde_ipld_dagcbor::to_vec(value)
.map_err(|e| ContentableError::SerializationFailed(format!("dag-cbor: {e}")))
}
#[cfg(feature = "dag-cbor")]
fn from_cbor_bytes_impl<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, ContentableError> {
serde_ipld_dagcbor::from_slice(bytes)
.map_err(|e| ContentableError::DeserializationFailed(format!("dag-cbor: {e}")))
}
impl Contentable for PayloadSort {
fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
to_json_bytes(self)
}
fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
from_json_bytes(bytes)
}
#[cfg(feature = "dag-cbor")]
fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
to_cbor_bytes_impl(self)
}
#[cfg(feature = "dag-cbor")]
fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
from_cbor_bytes_impl(bytes)
}
}
impl Contentable for Label {
fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
to_json_bytes(self)
}
fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
from_json_bytes(bytes)
}
#[cfg(feature = "dag-cbor")]
fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
to_cbor_bytes_impl(self)
}
#[cfg(feature = "dag-cbor")]
fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
from_cbor_bytes_impl(bytes)
}
}
impl Contentable for GlobalType {
fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
if !self.all_vars_bound() {
return Err(ContentableError::InvalidFormat(
"canonical serialization requires all recursion variables to be bound".to_string(),
));
}
let db = GlobalTypeDB::from(self).normalize();
to_json_bytes(&db)
}
fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
let free_vars = sorted_free_vars(self.free_vars());
let env: Vec<&str> = free_vars.iter().map(String::as_str).collect();
let db = GlobalTypeDB::from_global_type_with_env(self, &env).normalize();
let envelope = GlobalTemplateEnvelope { free_vars, db };
to_json_bytes(&envelope)
}
fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
let db: GlobalTypeDB = from_json_bytes(bytes)?;
Ok(global_from_de_bruijn(&db, &mut vec![]))
}
#[cfg(feature = "dag-cbor")]
fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
if !self.all_vars_bound() {
return Err(ContentableError::InvalidFormat(
"canonical serialization requires all recursion variables to be bound".to_string(),
));
}
let db = GlobalTypeDB::from(self).normalize();
to_cbor_bytes_impl(&db)
}
#[cfg(feature = "dag-cbor")]
fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
let db: GlobalTypeDB = from_cbor_bytes_impl(bytes)?;
Ok(global_from_de_bruijn(&db, &mut vec![]))
}
}
impl Contentable for LocalTypeR {
fn to_bytes(&self) -> Result<Vec<u8>, ContentableError> {
if !self.all_vars_bound() {
return Err(ContentableError::InvalidFormat(
"canonical serialization requires all recursion variables to be bound".to_string(),
));
}
let db = LocalTypeRDB::from(self).normalize();
to_json_bytes(&db)
}
fn to_template_bytes(&self) -> Result<Vec<u8>, ContentableError> {
let free_vars = sorted_free_vars(self.free_vars());
let env: Vec<&str> = free_vars.iter().map(String::as_str).collect();
let db = LocalTypeRDB::from_local_type_with_env(self, &env).normalize();
let envelope = LocalTemplateEnvelope { free_vars, db };
to_json_bytes(&envelope)
}
fn from_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
let db: LocalTypeRDB = from_json_bytes(bytes)?;
Ok(local_from_de_bruijn(&db, &mut vec![]))
}
#[cfg(feature = "dag-cbor")]
fn to_cbor_bytes(&self) -> Result<Vec<u8>, ContentableError> {
if !self.all_vars_bound() {
return Err(ContentableError::InvalidFormat(
"canonical serialization requires all recursion variables to be bound".to_string(),
));
}
let db = LocalTypeRDB::from(self).normalize();
to_cbor_bytes_impl(&db)
}
#[cfg(feature = "dag-cbor")]
fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, ContentableError> {
let db: LocalTypeRDB = from_cbor_bytes_impl(bytes)?;
Ok(local_from_de_bruijn(&db, &mut vec![]))
}
}
fn global_from_de_bruijn(db: &GlobalTypeDB, names: &mut Vec<String>) -> GlobalType {
match db {
GlobalTypeDB::End => GlobalType::End,
GlobalTypeDB::Comm {
sender,
receiver,
branches,
} => GlobalType::Comm {
sender: sender.clone(),
receiver: receiver.clone(),
branches: branches
.iter()
.map(|(l, cont)| (l.clone(), global_from_de_bruijn(cont, names)))
.collect(),
},
GlobalTypeDB::Rec(body) => {
let var_name = format!("t{}", names.len());
names.push(var_name.clone());
let body_converted = global_from_de_bruijn(body, names);
names.pop();
GlobalType::Mu {
var: var_name,
body: Box::new(body_converted),
}
}
GlobalTypeDB::Var(idx) => {
let name = names
.get(names.len().saturating_sub(1 + idx))
.cloned()
.unwrap_or_else(|| format!("free{idx}"));
GlobalType::Var(name)
}
}
}
fn local_from_de_bruijn(db: &LocalTypeRDB, names: &mut Vec<String>) -> LocalTypeR {
match db {
LocalTypeRDB::End => LocalTypeR::End,
LocalTypeRDB::Send { partner, branches } => LocalTypeR::Send {
partner: partner.clone(),
branches: branches
.iter()
.map(|(l, vt, cont)| (l.clone(), vt.clone(), local_from_de_bruijn(cont, names)))
.collect(),
},
LocalTypeRDB::Recv { partner, branches } => LocalTypeR::Recv {
partner: partner.clone(),
branches: branches
.iter()
.map(|(l, vt, cont)| (l.clone(), vt.clone(), local_from_de_bruijn(cont, names)))
.collect(),
},
LocalTypeRDB::Rec(body) => {
let var_name = format!("t{}", names.len());
names.push(var_name.clone());
let body_converted = local_from_de_bruijn(body, names);
names.pop();
LocalTypeR::Mu {
var: var_name,
body: Box::new(body_converted),
}
}
LocalTypeRDB::Var(idx) => {
let name = names
.get(names.len().saturating_sub(1 + idx))
.cloned()
.unwrap_or_else(|| format!("free{idx}"));
LocalTypeR::Var(name)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_content_id_helper() {
let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
let cid = g.content_id_default().unwrap();
assert_eq!(cid.algorithm(), "blake3");
}
#[test]
fn test_payload_sort_roundtrip() {
let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
let bytes = sort.to_bytes().unwrap();
let recovered = PayloadSort::from_bytes(&bytes).unwrap();
assert_eq!(sort, recovered);
}
#[test]
fn test_label_roundtrip() {
let label = Label::with_sort("data", PayloadSort::Nat);
let bytes = label.to_bytes().unwrap();
let recovered = Label::from_bytes(&bytes).unwrap();
assert_eq!(label, recovered);
}
#[test]
fn test_global_type_alpha_equivalence() {
let g1 = GlobalType::mu(
"x",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
);
let g2 = GlobalType::mu(
"y",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
);
assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
assert_eq!(
g1.content_id_default().unwrap(),
g2.content_id_default().unwrap()
);
}
#[test]
fn test_local_type_alpha_equivalence() {
let t1 = LocalTypeR::mu(
"x",
LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
);
let t2 = LocalTypeR::mu(
"y",
LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("y")),
);
assert_eq!(t1.to_bytes().unwrap(), t2.to_bytes().unwrap());
assert_eq!(
t1.content_id_default().unwrap(),
t2.content_id_default().unwrap()
);
}
#[test]
fn test_global_type_roundtrip() {
let g = GlobalType::mu(
"x",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
);
let bytes = g.to_bytes().unwrap();
let recovered = GlobalType::from_bytes(&bytes).unwrap();
assert_eq!(g.to_bytes().unwrap(), recovered.to_bytes().unwrap());
}
#[test]
fn test_local_type_roundtrip() {
let t = LocalTypeR::mu(
"x",
LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
);
let bytes = t.to_bytes().unwrap();
let recovered = LocalTypeR::from_bytes(&bytes).unwrap();
assert_eq!(t.to_bytes().unwrap(), recovered.to_bytes().unwrap());
}
#[test]
fn test_local_type_roundtrip_preserves_payload_annotation() {
let t = LocalTypeR::Send {
partner: "B".to_string(),
branches: vec![(
Label::new("msg"),
Some(crate::ValType::Nat),
LocalTypeR::Recv {
partner: "A".to_string(),
branches: vec![(
Label::new("ack"),
Some(crate::ValType::Bool),
LocalTypeR::End,
)],
},
)],
};
let bytes = t.to_bytes().unwrap();
let recovered = LocalTypeR::from_bytes(&bytes).unwrap();
assert_eq!(t, recovered);
}
#[test]
fn test_branch_ordering_normalized() {
let g1 = GlobalType::comm(
"A",
"B",
vec![
(Label::new("b"), GlobalType::End),
(Label::new("a"), GlobalType::End),
],
);
let g2 = GlobalType::comm(
"A",
"B",
vec![
(Label::new("a"), GlobalType::End),
(Label::new("b"), GlobalType::End),
],
);
assert_eq!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
}
#[test]
fn test_different_types_different_bytes() {
let g1 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
let g2 = GlobalType::send("A", "B", Label::new("other"), GlobalType::End);
assert_ne!(g1.to_bytes().unwrap(), g2.to_bytes().unwrap());
assert_ne!(
g1.content_id_default().unwrap(),
g2.content_id_default().unwrap()
);
}
#[test]
fn test_nested_recursion_content_id() {
let g1 = GlobalType::mu(
"x",
GlobalType::mu(
"y",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
),
);
let g2 = GlobalType::mu(
"a",
GlobalType::mu(
"b",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("b")),
),
);
assert_eq!(
g1.content_id_default().unwrap(),
g2.content_id_default().unwrap()
);
}
#[test]
fn test_different_binder_reference() {
let g1 = GlobalType::mu(
"x",
GlobalType::mu(
"y",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
),
);
let g2 = GlobalType::mu(
"x",
GlobalType::mu(
"y",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
),
);
assert_ne!(
g1.content_id_default().unwrap(),
g2.content_id_default().unwrap()
);
}
#[test]
fn test_global_type_open_term_rejected_for_canonical_serialization() {
let open = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("free_t"));
let err = open.to_bytes().expect_err("open terms must be rejected");
assert!(matches!(err, ContentableError::InvalidFormat(_)));
}
#[test]
fn test_local_type_open_term_rejected_for_canonical_serialization() {
let open = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("free_t"));
let err = open.to_bytes().expect_err("open terms must be rejected");
assert!(matches!(err, ContentableError::InvalidFormat(_)));
}
#[test]
fn test_global_type_open_term_has_template_id() {
let open = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("free_t"));
let tid = open
.template_id_default()
.expect("open terms should support template IDs");
let tid2 = open
.template_id_default()
.expect("template IDs should be deterministic");
assert_eq!(tid, tid2);
}
#[test]
fn test_local_type_open_term_has_template_id() {
let open = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("free_t"));
let tid = open
.template_id_default()
.expect("open terms should support template IDs");
let tid2 = open
.template_id_default()
.expect("template IDs should be deterministic");
assert_eq!(tid, tid2);
}
#[test]
fn test_template_id_distinguishes_free_variable_interfaces() {
let g1 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x"));
let g2 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y"));
assert_ne!(
g1.template_id_default().unwrap(),
g2.template_id_default().unwrap()
);
}
#[cfg(feature = "dag-cbor")]
mod cbor_tests {
use super::*;
#[test]
fn test_payload_sort_cbor_roundtrip() {
let sort = PayloadSort::prod(PayloadSort::Nat, PayloadSort::Bool);
let bytes = sort.to_cbor_bytes().unwrap();
let recovered = PayloadSort::from_cbor_bytes(&bytes).unwrap();
assert_eq!(sort, recovered);
}
#[test]
fn test_label_cbor_roundtrip() {
let label = Label::with_sort("data", PayloadSort::Nat);
let bytes = label.to_cbor_bytes().unwrap();
let recovered = Label::from_cbor_bytes(&bytes).unwrap();
assert_eq!(label, recovered);
}
#[test]
fn test_global_type_cbor_roundtrip() {
let g = GlobalType::mu(
"x",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
);
let bytes = g.to_cbor_bytes().unwrap();
let recovered = GlobalType::from_cbor_bytes(&bytes).unwrap();
assert_eq!(
g.to_cbor_bytes().unwrap(),
recovered.to_cbor_bytes().unwrap()
);
}
#[test]
fn test_local_type_cbor_roundtrip() {
let t = LocalTypeR::mu(
"x",
LocalTypeR::send("B", Label::new("msg"), LocalTypeR::var("x")),
);
let bytes = t.to_cbor_bytes().unwrap();
let recovered = LocalTypeR::from_cbor_bytes(&bytes).unwrap();
assert_eq!(
t.to_cbor_bytes().unwrap(),
recovered.to_cbor_bytes().unwrap()
);
}
#[test]
fn test_cbor_alpha_equivalence() {
let g1 = GlobalType::mu(
"x",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
);
let g2 = GlobalType::mu(
"y",
GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
);
assert_eq!(g1.to_cbor_bytes().unwrap(), g2.to_cbor_bytes().unwrap());
assert_eq!(
g1.content_id_cbor_default().unwrap(),
g2.content_id_cbor_default().unwrap()
);
}
#[test]
fn test_cbor_more_compact_than_json() {
let g = GlobalType::comm(
"A",
"B",
vec![
(Label::new("msg1"), GlobalType::End),
(Label::new("msg2"), GlobalType::End),
(Label::new("msg3"), GlobalType::End),
],
);
let json_bytes = g.to_bytes().unwrap();
let cbor_bytes = g.to_cbor_bytes().unwrap();
assert!(
cbor_bytes.len() < json_bytes.len(),
"CBOR ({} bytes) should be smaller than JSON ({} bytes)",
cbor_bytes.len(),
json_bytes.len()
);
}
#[test]
fn test_json_and_cbor_produce_different_bytes() {
let g = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
let json_bytes = g.to_bytes().unwrap();
let cbor_bytes = g.to_cbor_bytes().unwrap();
assert_ne!(json_bytes, cbor_bytes);
}
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
fn arb_var_name() -> impl Strategy<Value = String> {
prop_oneof![
Just("x".to_string()),
Just("y".to_string()),
Just("z".to_string()),
Just("t".to_string()),
Just("s".to_string()),
]
}
fn arb_role() -> impl Strategy<Value = String> {
prop_oneof![
Just("A".to_string()),
Just("B".to_string()),
Just("C".to_string()),
]
}
fn arb_label() -> impl Strategy<Value = Label> {
prop_oneof![
Just(Label::new("msg")),
Just(Label::new("data")),
Just(Label::new("ack")),
Just(Label::with_sort("value", PayloadSort::Nat)),
Just(Label::with_sort("flag", PayloadSort::Bool)),
]
}
#[allow(dead_code)]
fn arb_local_type(depth: usize) -> impl Strategy<Value = LocalTypeR> {
if depth == 0 {
prop_oneof![
Just(LocalTypeR::End),
arb_var_name().prop_map(LocalTypeR::var),
]
.boxed()
} else {
prop_oneof![
Just(LocalTypeR::End),
(arb_role(), arb_label(), arb_local_type(depth - 1))
.prop_map(|(partner, label, cont)| LocalTypeR::send(partner, label, cont)),
(arb_role(), arb_label(), arb_local_type(depth - 1))
.prop_map(|(partner, label, cont)| LocalTypeR::recv(partner, label, cont)),
(arb_var_name(), arb_local_type(depth - 1))
.prop_map(|(var, body)| LocalTypeR::mu(var, body)),
arb_var_name().prop_map(LocalTypeR::var),
]
.boxed()
}
}
fn rename_global_type(g: &GlobalType, mapping: &[(&str, &str)]) -> GlobalType {
fn rename_inner(
g: &GlobalType,
mapping: &[(&str, &str)],
bound: &mut Vec<(String, String)>,
) -> GlobalType {
match g {
GlobalType::End => GlobalType::End,
GlobalType::Comm {
sender,
receiver,
branches,
} => GlobalType::Comm {
sender: sender.clone(),
receiver: receiver.clone(),
branches: branches
.iter()
.map(|(l, cont)| (l.clone(), rename_inner(cont, mapping, bound)))
.collect(),
},
GlobalType::Mu { var, body } => {
let new_var = mapping
.iter()
.find(|(old, _)| *old == var)
.map(|(_, new)| (*new).to_string())
.unwrap_or_else(|| var.clone());
bound.push((var.clone(), new_var.clone()));
let new_body = rename_inner(body, mapping, bound);
bound.pop();
GlobalType::Mu {
var: new_var,
body: Box::new(new_body),
}
}
GlobalType::Var(name) => {
let new_name = bound
.iter()
.rev()
.find(|(old, _)| old == name)
.map(|(_, new)| new.clone())
.unwrap_or_else(|| name.clone());
GlobalType::Var(new_name)
}
}
}
rename_inner(g, mapping, &mut vec![])
}
fn arb_closed_global_type(depth: usize) -> impl Strategy<Value = GlobalType> {
arb_var_name().prop_flat_map(move |var| {
let var_clone = var.clone();
arb_global_type_closed_body(depth, var)
.prop_map(move |body| GlobalType::mu(var_clone.clone(), body))
})
}
fn arb_global_type_closed_body(
depth: usize,
bound_var: String,
) -> impl Strategy<Value = GlobalType> {
if depth == 0 {
prop_oneof![
Just(GlobalType::End),
Just(GlobalType::var(bound_var)), ]
.boxed()
} else {
let bv = bound_var.clone();
let bv2 = bound_var.clone();
prop_oneof![
Just(GlobalType::End),
Just(GlobalType::var(bv)),
(arb_role(), arb_role(), arb_label()).prop_flat_map(
move |(sender, receiver, label)| {
let bv_inner = bv2.clone();
arb_global_type_closed_body(depth - 1, bv_inner).prop_map(move |cont| {
GlobalType::send(sender.clone(), receiver.clone(), label.clone(), cont)
})
}
),
]
.boxed()
}
}
proptest! {
#[test]
fn prop_content_id_deterministic(g in arb_closed_global_type(3)) {
let cid1 = g.content_id_default().unwrap();
let cid2 = g.content_id_default().unwrap();
prop_assert_eq!(cid1, cid2);
}
#[test]
fn prop_to_bytes_deterministic(g in arb_closed_global_type(3)) {
let bytes1 = g.to_bytes().unwrap();
let bytes2 = g.to_bytes().unwrap();
prop_assert_eq!(bytes1, bytes2);
}
#[test]
fn prop_alpha_equivalence_closed(g in arb_closed_global_type(3)) {
let renamed = rename_global_type(&g, &[("x", "renamed_x"), ("y", "renamed_y"), ("t", "renamed_t")]);
prop_assert_eq!(
g.content_id_default().unwrap(),
renamed.content_id_default().unwrap(),
"α-equivalent closed types should have same content ID"
);
}
#[test]
fn prop_roundtrip_closed(g in arb_closed_global_type(3)) {
let bytes = g.to_bytes().unwrap();
if let Ok(recovered) = GlobalType::from_bytes(&bytes) {
prop_assert_eq!(
g.content_id_default().unwrap(),
recovered.content_id_default().unwrap(),
"roundtrip should preserve content ID for closed types"
);
}
}
#[test]
fn prop_branch_order_invariant(
sender in arb_role(),
receiver in arb_role(),
label1 in arb_label(),
label2 in arb_label(),
) {
let g1 = GlobalType::comm(
&sender, &receiver,
vec![
(label1.clone(), GlobalType::End),
(label2.clone(), GlobalType::End),
],
);
let g2 = GlobalType::comm(
&sender, &receiver,
vec![
(label2, GlobalType::End),
(label1, GlobalType::End),
],
);
prop_assert_eq!(
g1.content_id_default().unwrap(),
g2.content_id_default().unwrap(),
"branch order should not affect content ID"
);
}
#[test]
fn prop_local_type_alpha_equiv(
partner in arb_role(),
label in arb_label(),
) {
let t1 = LocalTypeR::mu("x", LocalTypeR::send(&partner, label.clone(), LocalTypeR::var("x")));
let t2 = LocalTypeR::mu("y", LocalTypeR::send(&partner, label, LocalTypeR::var("y")));
prop_assert_eq!(
t1.content_id_default().unwrap(),
t2.content_id_default().unwrap(),
"α-equivalent local types should have same content ID"
);
}
}
}