1use super::{Branch, Choreography, LocalType, MessageType, NonEmptyVec, Protocol};
22use crate::ast::Condition;
23use telltale_types::{GlobalType as GlobalTypeCore, Label, LocalTypeR, PayloadSort};
24use thiserror::Error;
25
26#[derive(Debug, Clone, Error)]
28pub enum ConversionError {
29 #[error("unsupported DSL feature: {feature}. {hint}")]
31 UnsupportedFeature { feature: String, hint: String },
32
33 #[error("choice has no branches")]
35 EmptyChoice,
36
37 #[error("invalid choice: branch '{label}' does not start with Send from decider")]
39 InvalidChoice { label: String },
40
41 #[error("choice has inconsistent receivers: expected all branches to send to {expected}, but branch '{label}' sends to {actual}")]
43 InconsistentReceivers {
44 expected: String,
45 actual: String,
46 label: String,
47 },
48}
49
50pub type ConversionResult<T> = Result<T, ConversionError>;
52
53pub fn choreography_to_global(choreography: &Choreography) -> ConversionResult<GlobalTypeCore> {
66 protocol_to_global(&choreography.protocol)
67}
68
69pub fn protocol_to_global(protocol: &Protocol) -> ConversionResult<GlobalTypeCore> {
84 match protocol {
85 Protocol::End => Ok(GlobalTypeCore::End),
86
87 Protocol::Var(ident) => Ok(GlobalTypeCore::var(ident.to_string())),
88
89 Protocol::Rec { label, body } => {
90 let body_global = protocol_to_global(body)?;
91 Ok(GlobalTypeCore::mu(label.to_string(), body_global))
92 }
93
94 Protocol::Send {
95 from,
96 to,
97 message,
98 continuation,
99 ..
100 } => convert_protocol_send(from, to, message, continuation),
101
102 Protocol::Choice {
103 role: decider,
104 branches,
105 ..
106 } => convert_protocol_choice(decider, branches),
107
108 Protocol::Broadcast { .. } => Err(ConversionError::UnsupportedFeature {
109 feature: "Broadcast".to_string(),
110 hint: "Desugar to nested Sends using desugar_broadcast() first".to_string(),
111 }),
112
113 Protocol::Loop { condition, body } => convert_protocol_loop(condition.as_ref(), body),
114
115 Protocol::Parallel { .. } => Err(ConversionError::UnsupportedFeature {
116 feature: "Parallel".to_string(),
117 hint: "Parallel composition has no theory equivalent".to_string(),
118 }),
119
120 Protocol::Begin { .. }
121 | Protocol::Await { .. }
122 | Protocol::Resolve { .. }
123 | Protocol::Invalidate { .. } => Err(ConversionError::UnsupportedFeature {
124 feature: "CommitmentLifecycle".to_string(),
125 hint:
126 "Explicit commitment lifecycle requires protocol-machine lowering instead of theory conversion"
127 .to_string(),
128 }),
129
130 Protocol::Let { continuation, .. } => protocol_to_global(continuation),
131
132 Protocol::Case { .. } => Err(ConversionError::UnsupportedFeature {
133 feature: "Case".to_string(),
134 hint: "Authority-local case/of must be lowered to explicit protocol choices first"
135 .to_string(),
136 }),
137
138 Protocol::Timeout { .. } => Err(ConversionError::UnsupportedFeature {
139 feature: "Timeout".to_string(),
140 hint: "Timeout blocks must be lowered to explicit protocol choices before theory conversion"
141 .to_string(),
142 }),
143
144 Protocol::Publish { .. } => Err(ConversionError::UnsupportedFeature {
145 feature: "Publish".to_string(),
146 hint: "Publication surfaces must be erased or lowered before theory conversion"
147 .to_string(),
148 }),
149
150 Protocol::PublishAuthority { .. } => Err(ConversionError::UnsupportedFeature {
151 feature: "PublishAuthority".to_string(),
152 hint: "Canonical publication requires protocol-machine lowering before theory conversion"
153 .to_string(),
154 }),
155
156 Protocol::Materialize { .. } => Err(ConversionError::UnsupportedFeature {
157 feature: "Materialize".to_string(),
158 hint:
159 "Materialization requires protocol-machine lowering before theory conversion"
160 .to_string(),
161 }),
162
163 Protocol::Handoff { .. } => Err(ConversionError::UnsupportedFeature {
164 feature: "Handoff".to_string(),
165 hint: "Semantic handoff must be lowered before theory conversion".to_string(),
166 }),
167
168 Protocol::DependentWork { .. } => Err(ConversionError::UnsupportedFeature {
169 feature: "DependentWork".to_string(),
170 hint: "Dependent work declarations must be lowered before theory conversion"
171 .to_string(),
172 }),
173
174 Protocol::Extension { .. } => Err(ConversionError::UnsupportedFeature {
175 feature: "Extension".to_string(),
176 hint: "Protocol extensions have no theory equivalent".to_string(),
177 }),
178 }
179}
180
181fn convert_protocol_send(
182 from: &super::Role,
183 to: &super::Role,
184 message: &MessageType,
185 continuation: &Protocol,
186) -> ConversionResult<GlobalTypeCore> {
187 let cont_global = protocol_to_global(continuation)?;
188 let label = message_to_label(message);
189 Ok(GlobalTypeCore::send(
190 from.name().to_string(),
191 to.name().to_string(),
192 label,
193 cont_global,
194 ))
195}
196
197fn convert_protocol_choice(
198 decider: &super::Role,
199 branches: &NonEmptyVec<Branch>,
200) -> ConversionResult<GlobalTypeCore> {
201 let first_receiver = extract_receiver(&branches[0], decider)?;
202 let mut global_branches = Vec::with_capacity(branches.len());
203 for branch in branches {
204 let (label, cont) = convert_choice_branch(branch, decider, &first_receiver)?;
205 global_branches.push((label, cont));
206 }
207 Ok(GlobalTypeCore::comm(
208 decider.name().to_string(),
209 first_receiver,
210 global_branches,
211 ))
212}
213
214fn extract_receiver(branch: &Branch, decider: &super::Role) -> ConversionResult<String> {
216 match &branch.protocol {
217 Protocol::Send { from, to, .. } => {
218 if from.name() != decider.name() {
219 return Err(ConversionError::InvalidChoice {
220 label: branch.label.to_string(),
221 });
222 }
223 Ok(to.name().to_string())
224 }
225 _ => Err(ConversionError::InvalidChoice {
226 label: branch.label.to_string(),
227 }),
228 }
229}
230
231fn convert_choice_branch(
233 branch: &Branch,
234 decider: &super::Role,
235 expected_receiver: &str,
236) -> ConversionResult<(Label, GlobalTypeCore)> {
237 match &branch.protocol {
238 Protocol::Send {
239 from,
240 to,
241 message,
242 continuation,
243 ..
244 } => {
245 if from.name() != decider.name() {
246 return Err(ConversionError::InvalidChoice {
247 label: branch.label.to_string(),
248 });
249 }
250 if *to.name() != expected_receiver {
251 return Err(ConversionError::InconsistentReceivers {
252 expected: expected_receiver.to_string(),
253 actual: to.name().to_string(),
254 label: branch.label.to_string(),
255 });
256 }
257
258 let label = Label::new(branch.label.to_string());
259 let cont_global = if branch.label == message.name {
260 protocol_to_global(continuation)?
261 } else {
262 protocol_to_global(&branch.protocol)?
263 };
264 Ok((label, cont_global))
265 }
266 _ => Err(ConversionError::InvalidChoice {
267 label: branch.label.to_string(),
268 }),
269 }
270}
271
272fn append_global_continuation(
273 global: GlobalTypeCore,
274 continuation: GlobalTypeCore,
275) -> GlobalTypeCore {
276 match global {
277 GlobalTypeCore::End => continuation,
278 GlobalTypeCore::Comm {
279 sender,
280 receiver,
281 branches,
282 } => GlobalTypeCore::Comm {
283 sender,
284 receiver,
285 branches: branches
286 .into_iter()
287 .map(|(label, branch)| {
288 (
289 label,
290 append_global_continuation(branch, continuation.clone()),
291 )
292 })
293 .collect(),
294 },
295 GlobalTypeCore::Mu { var, body } => GlobalTypeCore::Mu {
296 var,
297 body: Box::new(append_global_continuation(*body, continuation)),
298 },
299 GlobalTypeCore::Var(var) => GlobalTypeCore::Var(var),
300 }
301}
302
303fn repeat_global(body: &Protocol, iterations: usize) -> ConversionResult<GlobalTypeCore> {
304 if iterations == 0 {
305 return Ok(GlobalTypeCore::End);
306 }
307
308 let body_global = protocol_to_global(body)?;
309 let mut repeated = GlobalTypeCore::End;
310 for _ in 0..iterations {
311 repeated = append_global_continuation(body_global.clone(), repeated);
312 }
313 Ok(repeated)
314}
315
316fn convert_protocol_loop(
317 condition: Option<&Condition>,
318 body: &Protocol,
319) -> ConversionResult<GlobalTypeCore> {
320 match condition {
321 Some(Condition::Count(iterations))
322 | Some(Condition::Fuel(iterations))
323 | Some(Condition::YieldAfter(iterations)) => repeat_global(body, *iterations),
324 Some(Condition::RoleDecides(_)) => Err(ConversionError::UnsupportedFeature {
325 feature: "Loop".to_string(),
326 hint: "Role-decides loops should be desugared to choice+recursion before theory conversion"
327 .to_string(),
328 }),
329 Some(Condition::Custom(_)) | Some(Condition::YieldWhen(_)) | None => {
330 Err(ConversionError::UnsupportedFeature {
331 feature: "Loop".to_string(),
332 hint: "Only finite counted loops currently lower to GlobalType; use recursion for open-ended loops"
333 .to_string(),
334 })
335 }
336 }
337}
338
339fn message_to_label(message: &MessageType) -> Label {
341 let name = message.name.to_string();
344
345 Label::with_sort(name, PayloadSort::Unit)
347}
348
349pub fn local_to_local_r(local: &LocalType) -> ConversionResult<LocalTypeR> {
369 match local {
370 LocalType::End => Ok(LocalTypeR::End),
371
372 LocalType::Var(ident) => Ok(LocalTypeR::Var(ident.to_string())),
373
374 LocalType::Send {
375 to,
376 message,
377 continuation,
378 } => convert_local_send(to, message, continuation),
379
380 LocalType::Receive {
381 from,
382 message,
383 continuation,
384 } => convert_local_receive(from, message, continuation),
385
386 LocalType::Select { to, branches } => convert_local_select(to, branches),
387
388 LocalType::Branch { from, branches } => convert_local_branch(from, branches),
389
390 LocalType::Rec { label, body } => {
391 let body_r = local_to_local_r(body)?;
392 Ok(LocalTypeR::mu(label.to_string(), body_r))
393 }
394
395 LocalType::LocalChoice { .. } => Err(ConversionError::UnsupportedFeature {
396 feature: "LocalChoice".to_string(),
397 hint: "LocalChoice (local decisions without communication) has no theory equivalent"
398 .to_string(),
399 }),
400
401 LocalType::Loop { condition, body } => convert_local_loop(condition.as_ref(), body),
402
403 LocalType::Timeout { .. } => Err(ConversionError::UnsupportedFeature {
404 feature: "Timeout".to_string(),
405 hint: "Timeout is a DSL extension with no theory equivalent".to_string(),
406 }),
407 }
408}
409
410fn convert_local_send(
411 to: &super::Role,
412 message: &MessageType,
413 continuation: &LocalType,
414) -> ConversionResult<LocalTypeR> {
415 let cont_r = local_to_local_r(continuation)?;
416 let label = message_to_label(message);
417 Ok(LocalTypeR::send(to.name().to_string(), label, cont_r))
418}
419
420fn convert_local_receive(
421 from: &super::Role,
422 message: &MessageType,
423 continuation: &LocalType,
424) -> ConversionResult<LocalTypeR> {
425 let cont_r = local_to_local_r(continuation)?;
426 let label = message_to_label(message);
427 Ok(LocalTypeR::recv(from.name().to_string(), label, cont_r))
428}
429
430fn convert_local_select(
431 to: &super::Role,
432 branches: &[(proc_macro2::Ident, LocalType)],
433) -> ConversionResult<LocalTypeR> {
434 let mut r_branches = Vec::with_capacity(branches.len());
435 for (ident, cont) in branches {
436 let cont_r = match cont {
440 LocalType::Send {
441 to: send_to,
442 message,
443 continuation,
444 } if send_to.name() == to.name() && message.name == *ident => {
445 local_to_local_r(continuation)?
446 }
447 _ => local_to_local_r(cont)?,
448 };
449 r_branches.push((Label::new(ident.to_string()), None, cont_r));
450 }
451 Ok(LocalTypeR::Send {
452 partner: to.name().to_string(),
453 branches: r_branches,
454 })
455}
456
457fn convert_local_branch(
458 from: &super::Role,
459 branches: &[(proc_macro2::Ident, LocalType)],
460) -> ConversionResult<LocalTypeR> {
461 let mut r_branches = Vec::with_capacity(branches.len());
462 for (ident, cont) in branches {
463 let cont_r = match cont {
467 LocalType::Receive {
468 from: recv_from,
469 message,
470 continuation,
471 } if recv_from.name() == from.name() && message.name == *ident => {
472 local_to_local_r(continuation)?
473 }
474 _ => local_to_local_r(cont)?,
475 };
476 r_branches.push((Label::new(ident.to_string()), None, cont_r));
477 }
478 Ok(LocalTypeR::Recv {
479 partner: from.name().to_string(),
480 branches: r_branches,
481 })
482}
483
484fn append_local_r_continuation(local: LocalTypeR, continuation: LocalTypeR) -> LocalTypeR {
485 match local {
486 LocalTypeR::End => continuation,
487 LocalTypeR::Send { partner, branches } => LocalTypeR::Send {
488 partner,
489 branches: branches
490 .into_iter()
491 .map(|(label, payload, branch)| {
492 (
493 label,
494 payload,
495 append_local_r_continuation(branch, continuation.clone()),
496 )
497 })
498 .collect(),
499 },
500 LocalTypeR::Recv { partner, branches } => LocalTypeR::Recv {
501 partner,
502 branches: branches
503 .into_iter()
504 .map(|(label, payload, branch)| {
505 (
506 label,
507 payload,
508 append_local_r_continuation(branch, continuation.clone()),
509 )
510 })
511 .collect(),
512 },
513 LocalTypeR::Mu { var, body } => LocalTypeR::Mu {
514 var,
515 body: Box::new(append_local_r_continuation(*body, continuation)),
516 },
517 LocalTypeR::Var(var) => LocalTypeR::Var(var),
518 }
519}
520
521fn repeat_local_r(body: &LocalType, iterations: usize) -> ConversionResult<LocalTypeR> {
522 if iterations == 0 {
523 return Ok(LocalTypeR::End);
524 }
525
526 let body_r = local_to_local_r(body)?;
527 let mut repeated = LocalTypeR::End;
528 for _ in 0..iterations {
529 repeated = append_local_r_continuation(body_r.clone(), repeated);
530 }
531 Ok(repeated)
532}
533
534fn convert_local_loop(
535 condition: Option<&Condition>,
536 body: &LocalType,
537) -> ConversionResult<LocalTypeR> {
538 match condition {
539 Some(Condition::Count(iterations))
540 | Some(Condition::Fuel(iterations))
541 | Some(Condition::YieldAfter(iterations)) => repeat_local_r(body, *iterations),
542 Some(Condition::RoleDecides(_)) => Err(ConversionError::UnsupportedFeature {
543 feature: "Loop".to_string(),
544 hint: "Role-decides loops should be desugared to choice+recursion before theory conversion"
545 .to_string(),
546 }),
547 Some(Condition::Custom(_)) | Some(Condition::YieldWhen(_)) | None => {
548 Err(ConversionError::UnsupportedFeature {
549 feature: "Loop".to_string(),
550 hint: "Only finite counted loops currently lower to LocalTypeR; use recursion for open-ended loops"
551 .to_string(),
552 })
553 }
554 }
555}
556
557pub fn local_types_equivalent(lt1: &LocalTypeR, lt2: &LocalTypeR) -> bool {
567 match (lt1, lt2) {
568 (LocalTypeR::End, LocalTypeR::End) => true,
569
570 (LocalTypeR::Var(v1), LocalTypeR::Var(v2)) => v1 == v2,
571
572 (LocalTypeR::Mu { var: v1, body: b1 }, LocalTypeR::Mu { var: v2, body: b2 }) => {
573 v1 == v2 && local_types_equivalent(b1, b2)
575 }
576
577 (
578 LocalTypeR::Send {
579 partner: p1,
580 branches: bs1,
581 },
582 LocalTypeR::Send {
583 partner: p2,
584 branches: bs2,
585 },
586 ) => {
587 p1 == p2
588 && bs1.len() == bs2.len()
589 && bs1
590 .iter()
591 .zip(bs2.iter())
592 .all(|((l1, _vt1, c1), (l2, _vt2, c2))| {
593 l1.name == l2.name && local_types_equivalent(c1, c2)
594 })
595 }
596
597 (
598 LocalTypeR::Recv {
599 partner: p1,
600 branches: bs1,
601 },
602 LocalTypeR::Recv {
603 partner: p2,
604 branches: bs2,
605 },
606 ) => {
607 p1 == p2
608 && bs1.len() == bs2.len()
609 && bs1
610 .iter()
611 .zip(bs2.iter())
612 .all(|((l1, _vt1, c1), (l2, _vt2, c2))| {
613 l1.name == l2.name && local_types_equivalent(c1, c2)
614 })
615 }
616
617 _ => false,
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::super::annotation::Annotations;
624 use super::super::NonEmptyVec;
625 use super::*;
626 use proc_macro2::Ident;
627 use proc_macro2::Span;
628
629 fn ident(s: &str) -> Ident {
630 Ident::new(s, Span::call_site())
631 }
632
633 fn role(name: &str) -> super::super::Role {
634 super::super::Role::new(ident(name)).unwrap()
635 }
636
637 fn message(name: &str) -> MessageType {
638 MessageType {
639 name: ident(name),
640 type_annotation: None,
641 payload: None,
642 }
643 }
644
645 #[test]
646 fn test_simple_send_conversion() {
647 let protocol = Protocol::Send {
649 from: role("A"),
650 to: role("B"),
651 message: message("msg"),
652 continuation: Box::new(Protocol::End),
653 annotations: Annotations::new(),
654 from_annotations: Annotations::new(),
655 to_annotations: Annotations::new(),
656 };
657
658 let global = protocol_to_global(&protocol).unwrap();
659 assert!(
660 matches!(global, GlobalTypeCore::Comm { sender, receiver, .. }
661 if sender == "A" && receiver == "B")
662 );
663 }
664
665 #[test]
666 fn test_recursive_conversion() {
667 let protocol = Protocol::Rec {
669 label: ident("Loop"),
670 body: Box::new(Protocol::Send {
671 from: role("A"),
672 to: role("B"),
673 message: message("msg"),
674 continuation: Box::new(Protocol::Var(ident("Loop"))),
675 annotations: Annotations::new(),
676 from_annotations: Annotations::new(),
677 to_annotations: Annotations::new(),
678 }),
679 };
680
681 let global = protocol_to_global(&protocol).unwrap();
682 assert!(matches!(global, GlobalTypeCore::Mu { var, .. } if var == "Loop"));
683 }
684
685 #[test]
686 fn test_broadcast_unsupported() {
687 let protocol = Protocol::Broadcast {
688 from: role("A"),
689 to_all: NonEmptyVec::from_head_tail(role("B"), vec![role("C")]),
690 message: message("msg"),
691 continuation: Box::new(Protocol::End),
692 annotations: Annotations::new(),
693 from_annotations: Annotations::new(),
694 };
695
696 let result = protocol_to_global(&protocol);
697 assert!(
698 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Broadcast")
699 );
700 }
701
702 #[test]
703 fn test_loop_unsupported() {
704 let protocol = Protocol::Loop {
705 condition: None,
706 body: Box::new(Protocol::End),
707 };
708 let result = protocol_to_global(&protocol);
709 assert!(
710 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Loop")
711 );
712 }
713
714 #[test]
715 fn test_parallel_unsupported() {
716 let protocol = Protocol::Parallel {
717 protocols: NonEmptyVec::from_head_tail(Protocol::End, vec![Protocol::End]),
718 };
719 let result = protocol_to_global(&protocol);
720 assert!(
721 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Parallel")
722 );
723 }
724
725 #[test]
726 fn test_local_type_conversion() {
727 let local = LocalType::Send {
729 to: role("B"),
730 message: message("msg"),
731 continuation: Box::new(LocalType::End),
732 };
733
734 let local_r = local_to_local_r(&local).unwrap();
735 assert!(matches!(local_r, LocalTypeR::Send { partner, .. } if partner == "B"));
736 }
737
738 #[test]
739 fn test_localchoice_unsupported() {
740 let local = LocalType::LocalChoice {
741 branches: vec![(ident("L"), LocalType::End)],
742 };
743 let result = local_to_local_r(&local);
744 assert!(
745 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "LocalChoice")
746 );
747 }
748
749 #[test]
750 fn test_timeout_unsupported() {
751 let local = LocalType::Timeout {
752 duration: std::time::Duration::from_millis(100),
753 body: Box::new(LocalType::End),
754 on_timeout: Box::new(LocalType::End),
755 on_cancel: None,
756 };
757 let result = local_to_local_r(&local);
758 assert!(
759 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Timeout")
760 );
761 }
762
763 #[test]
764 fn test_message_conversion_uses_unit_payload_sort() {
765 let protocol = Protocol::Send {
766 from: role("A"),
767 to: role("B"),
768 message: MessageType {
769 name: ident("payload_msg"),
770 type_annotation: None,
771 payload: Some(quote::quote! { Vec<u8> }),
772 },
773 continuation: Box::new(Protocol::End),
774 annotations: Annotations::new(),
775 from_annotations: Annotations::new(),
776 to_annotations: Annotations::new(),
777 };
778
779 let global = protocol_to_global(&protocol).expect("conversion should succeed");
780 match global {
781 GlobalTypeCore::Comm { branches, .. } => {
782 let (label, _) = &branches[0];
783 assert_eq!(label.sort, PayloadSort::Unit);
784 }
785 _ => panic!("expected comm"),
786 }
787 }
788
789 #[test]
790 fn test_equivalence_check() {
791 let lt1 = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
792 let lt2 = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
793 let lt3 = LocalTypeR::send("C", Label::new("msg"), LocalTypeR::End);
794
795 assert!(local_types_equivalent(<1, <2));
796 assert!(!local_types_equivalent(<1, <3));
797 }
798}