1use super::{Branch, Choreography, LocalType, MessageType, NonEmptyVec, Protocol};
22use telltale_types::{GlobalType as GlobalTypeCore, Label, LocalTypeR, PayloadSort};
23use thiserror::Error;
24
25#[derive(Debug, Clone, Error)]
27pub enum ConversionError {
28 #[error("unsupported DSL feature: {feature}. {hint}")]
30 UnsupportedFeature { feature: String, hint: String },
31
32 #[error("choice has no branches")]
34 EmptyChoice,
35
36 #[error("invalid choice: branch '{label}' does not start with Send from decider")]
38 InvalidChoice { label: String },
39
40 #[error("choice has inconsistent receivers: expected all branches to send to {expected}, but branch '{label}' sends to {actual}")]
42 InconsistentReceivers {
43 expected: String,
44 actual: String,
45 label: String,
46 },
47}
48
49pub type ConversionResult<T> = Result<T, ConversionError>;
51
52pub fn choreography_to_global(choreography: &Choreography) -> ConversionResult<GlobalTypeCore> {
65 protocol_to_global(&choreography.protocol)
66}
67
68pub fn protocol_to_global(protocol: &Protocol) -> ConversionResult<GlobalTypeCore> {
83 match protocol {
84 Protocol::End => Ok(GlobalTypeCore::End),
85
86 Protocol::Var(ident) => Ok(GlobalTypeCore::var(ident.to_string())),
87
88 Protocol::Rec { label, body } => {
89 let body_global = protocol_to_global(body)?;
90 Ok(GlobalTypeCore::mu(label.to_string(), body_global))
91 }
92
93 Protocol::Send {
94 from,
95 to,
96 message,
97 continuation,
98 ..
99 } => convert_protocol_send(from, to, message, continuation),
100
101 Protocol::Choice {
102 role: decider,
103 branches,
104 ..
105 } => convert_protocol_choice(decider, branches),
106
107 Protocol::Broadcast { .. } => Err(ConversionError::UnsupportedFeature {
108 feature: "Broadcast".to_string(),
109 hint: "Desugar to nested Sends using desugar_broadcast() first".to_string(),
110 }),
111
112 Protocol::Loop { .. } => Err(ConversionError::UnsupportedFeature {
113 feature: "Loop".to_string(),
114 hint: "Convert to Rec/Var using desugar_loop() first".to_string(),
115 }),
116
117 Protocol::Parallel { .. } => Err(ConversionError::UnsupportedFeature {
118 feature: "Parallel".to_string(),
119 hint: "Parallel composition has no theory equivalent".to_string(),
120 }),
121
122 Protocol::Begin { .. }
123 | Protocol::Await { .. }
124 | Protocol::Resolve { .. }
125 | Protocol::Invalidate { .. } => Err(ConversionError::UnsupportedFeature {
126 feature: "CommitmentLifecycle".to_string(),
127 hint:
128 "Explicit commitment lifecycle requires protocol-machine lowering instead of theory conversion"
129 .to_string(),
130 }),
131
132 Protocol::Let { .. } => Err(ConversionError::UnsupportedFeature {
133 feature: "Let".to_string(),
134 hint: "Authority-local let bindings must be erased or lowered before theory conversion"
135 .to_string(),
136 }),
137
138 Protocol::Case { .. } => Err(ConversionError::UnsupportedFeature {
139 feature: "Case".to_string(),
140 hint: "Authority-local case/of must be lowered to explicit protocol choices first"
141 .to_string(),
142 }),
143
144 Protocol::Timeout { .. } => Err(ConversionError::UnsupportedFeature {
145 feature: "Timeout".to_string(),
146 hint: "Timeout blocks must be lowered to explicit protocol choices before theory conversion"
147 .to_string(),
148 }),
149
150 Protocol::Publish { .. } => Err(ConversionError::UnsupportedFeature {
151 feature: "Publish".to_string(),
152 hint: "Publication surfaces must be erased or lowered before theory conversion"
153 .to_string(),
154 }),
155
156 Protocol::PublishAuthority { .. } => Err(ConversionError::UnsupportedFeature {
157 feature: "PublishAuthority".to_string(),
158 hint: "Canonical publication requires protocol-machine lowering before theory conversion"
159 .to_string(),
160 }),
161
162 Protocol::Materialize { .. } => Err(ConversionError::UnsupportedFeature {
163 feature: "Materialize".to_string(),
164 hint:
165 "Materialization requires protocol-machine lowering before theory conversion"
166 .to_string(),
167 }),
168
169 Protocol::Handoff { .. } => Err(ConversionError::UnsupportedFeature {
170 feature: "Handoff".to_string(),
171 hint: "Semantic handoff must be lowered before theory conversion".to_string(),
172 }),
173
174 Protocol::DependentWork { .. } => Err(ConversionError::UnsupportedFeature {
175 feature: "DependentWork".to_string(),
176 hint: "Dependent work declarations must be lowered before theory conversion"
177 .to_string(),
178 }),
179
180 Protocol::Extension { .. } => Err(ConversionError::UnsupportedFeature {
181 feature: "Extension".to_string(),
182 hint: "Protocol extensions have no theory equivalent".to_string(),
183 }),
184 }
185}
186
187fn convert_protocol_send(
188 from: &super::Role,
189 to: &super::Role,
190 message: &MessageType,
191 continuation: &Protocol,
192) -> ConversionResult<GlobalTypeCore> {
193 let cont_global = protocol_to_global(continuation)?;
194 let label = message_to_label(message);
195 Ok(GlobalTypeCore::send(
196 from.name().to_string(),
197 to.name().to_string(),
198 label,
199 cont_global,
200 ))
201}
202
203fn convert_protocol_choice(
204 decider: &super::Role,
205 branches: &NonEmptyVec<Branch>,
206) -> ConversionResult<GlobalTypeCore> {
207 let first_receiver = extract_receiver(&branches[0], decider)?;
208 let mut global_branches = Vec::with_capacity(branches.len());
209 for branch in branches {
210 let (label, cont) = convert_choice_branch(branch, decider, &first_receiver)?;
211 global_branches.push((label, cont));
212 }
213 Ok(GlobalTypeCore::comm(
214 decider.name().to_string(),
215 first_receiver,
216 global_branches,
217 ))
218}
219
220fn extract_receiver(branch: &Branch, decider: &super::Role) -> ConversionResult<String> {
222 match &branch.protocol {
223 Protocol::Send { from, to, .. } => {
224 if from.name() != decider.name() {
225 return Err(ConversionError::InvalidChoice {
226 label: branch.label.to_string(),
227 });
228 }
229 Ok(to.name().to_string())
230 }
231 _ => Err(ConversionError::InvalidChoice {
232 label: branch.label.to_string(),
233 }),
234 }
235}
236
237fn convert_choice_branch(
239 branch: &Branch,
240 decider: &super::Role,
241 expected_receiver: &str,
242) -> ConversionResult<(Label, GlobalTypeCore)> {
243 match &branch.protocol {
244 Protocol::Send {
245 from,
246 to,
247 message,
248 continuation,
249 ..
250 } => {
251 if from.name() != decider.name() {
252 return Err(ConversionError::InvalidChoice {
253 label: branch.label.to_string(),
254 });
255 }
256 if *to.name() != expected_receiver {
257 return Err(ConversionError::InconsistentReceivers {
258 expected: expected_receiver.to_string(),
259 actual: to.name().to_string(),
260 label: branch.label.to_string(),
261 });
262 }
263
264 let cont_global = protocol_to_global(continuation)?;
265 let label = message_to_label(message);
266 Ok((label, cont_global))
267 }
268 _ => Err(ConversionError::InvalidChoice {
269 label: branch.label.to_string(),
270 }),
271 }
272}
273
274fn message_to_label(message: &MessageType) -> Label {
276 let name = message.name.to_string();
279
280 Label::with_sort(name, PayloadSort::Unit)
282}
283
284pub fn local_to_local_r(local: &LocalType) -> ConversionResult<LocalTypeR> {
304 match local {
305 LocalType::End => Ok(LocalTypeR::End),
306
307 LocalType::Var(ident) => Ok(LocalTypeR::Var(ident.to_string())),
308
309 LocalType::Send {
310 to,
311 message,
312 continuation,
313 } => convert_local_send(to, message, continuation),
314
315 LocalType::Receive {
316 from,
317 message,
318 continuation,
319 } => convert_local_receive(from, message, continuation),
320
321 LocalType::Select { to, branches } => convert_local_select(to, branches),
322
323 LocalType::Branch { from, branches } => convert_local_branch(from, branches),
324
325 LocalType::Rec { label, body } => {
326 let body_r = local_to_local_r(body)?;
327 Ok(LocalTypeR::mu(label.to_string(), body_r))
328 }
329
330 LocalType::LocalChoice { .. } => Err(ConversionError::UnsupportedFeature {
331 feature: "LocalChoice".to_string(),
332 hint: "LocalChoice (local decisions without communication) has no theory equivalent"
333 .to_string(),
334 }),
335
336 LocalType::Loop { .. } => Err(ConversionError::UnsupportedFeature {
337 feature: "Loop".to_string(),
338 hint: "Loop should be converted to Rec/Var before conversion".to_string(),
339 }),
340
341 LocalType::Timeout { .. } => Err(ConversionError::UnsupportedFeature {
342 feature: "Timeout".to_string(),
343 hint: "Timeout is a DSL extension with no theory equivalent".to_string(),
344 }),
345 }
346}
347
348fn convert_local_send(
349 to: &super::Role,
350 message: &MessageType,
351 continuation: &LocalType,
352) -> ConversionResult<LocalTypeR> {
353 let cont_r = local_to_local_r(continuation)?;
354 let label = message_to_label(message);
355 Ok(LocalTypeR::send(to.name().to_string(), label, cont_r))
356}
357
358fn convert_local_receive(
359 from: &super::Role,
360 message: &MessageType,
361 continuation: &LocalType,
362) -> ConversionResult<LocalTypeR> {
363 let cont_r = local_to_local_r(continuation)?;
364 let label = message_to_label(message);
365 Ok(LocalTypeR::recv(from.name().to_string(), label, cont_r))
366}
367
368fn convert_local_select(
369 to: &super::Role,
370 branches: &[(proc_macro2::Ident, LocalType)],
371) -> ConversionResult<LocalTypeR> {
372 let mut r_branches = Vec::with_capacity(branches.len());
373 for (ident, cont) in branches {
374 let cont_r = match cont {
378 LocalType::Send {
379 to: send_to,
380 message,
381 continuation,
382 } if send_to.name() == to.name() && message.name == *ident => {
383 local_to_local_r(continuation)?
384 }
385 _ => local_to_local_r(cont)?,
386 };
387 r_branches.push((Label::new(ident.to_string()), None, cont_r));
388 }
389 Ok(LocalTypeR::Send {
390 partner: to.name().to_string(),
391 branches: r_branches,
392 })
393}
394
395fn convert_local_branch(
396 from: &super::Role,
397 branches: &[(proc_macro2::Ident, LocalType)],
398) -> ConversionResult<LocalTypeR> {
399 let mut r_branches = Vec::with_capacity(branches.len());
400 for (ident, cont) in branches {
401 let cont_r = match cont {
405 LocalType::Receive {
406 from: recv_from,
407 message,
408 continuation,
409 } if recv_from.name() == from.name() && message.name == *ident => {
410 local_to_local_r(continuation)?
411 }
412 _ => local_to_local_r(cont)?,
413 };
414 r_branches.push((Label::new(ident.to_string()), None, cont_r));
415 }
416 Ok(LocalTypeR::Recv {
417 partner: from.name().to_string(),
418 branches: r_branches,
419 })
420}
421
422pub fn local_types_equivalent(lt1: &LocalTypeR, lt2: &LocalTypeR) -> bool {
432 match (lt1, lt2) {
433 (LocalTypeR::End, LocalTypeR::End) => true,
434
435 (LocalTypeR::Var(v1), LocalTypeR::Var(v2)) => v1 == v2,
436
437 (LocalTypeR::Mu { var: v1, body: b1 }, LocalTypeR::Mu { var: v2, body: b2 }) => {
438 v1 == v2 && local_types_equivalent(b1, b2)
440 }
441
442 (
443 LocalTypeR::Send {
444 partner: p1,
445 branches: bs1,
446 },
447 LocalTypeR::Send {
448 partner: p2,
449 branches: bs2,
450 },
451 ) => {
452 p1 == p2
453 && bs1.len() == bs2.len()
454 && bs1
455 .iter()
456 .zip(bs2.iter())
457 .all(|((l1, _vt1, c1), (l2, _vt2, c2))| {
458 l1.name == l2.name && local_types_equivalent(c1, c2)
459 })
460 }
461
462 (
463 LocalTypeR::Recv {
464 partner: p1,
465 branches: bs1,
466 },
467 LocalTypeR::Recv {
468 partner: p2,
469 branches: bs2,
470 },
471 ) => {
472 p1 == p2
473 && bs1.len() == bs2.len()
474 && bs1
475 .iter()
476 .zip(bs2.iter())
477 .all(|((l1, _vt1, c1), (l2, _vt2, c2))| {
478 l1.name == l2.name && local_types_equivalent(c1, c2)
479 })
480 }
481
482 _ => false,
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::super::annotation::Annotations;
489 use super::super::NonEmptyVec;
490 use super::*;
491 use proc_macro2::Ident;
492 use proc_macro2::Span;
493
494 fn ident(s: &str) -> Ident {
495 Ident::new(s, Span::call_site())
496 }
497
498 fn role(name: &str) -> super::super::Role {
499 super::super::Role::new(ident(name)).unwrap()
500 }
501
502 fn message(name: &str) -> MessageType {
503 MessageType {
504 name: ident(name),
505 type_annotation: None,
506 payload: None,
507 }
508 }
509
510 #[test]
511 fn test_simple_send_conversion() {
512 let protocol = Protocol::Send {
514 from: role("A"),
515 to: role("B"),
516 message: message("msg"),
517 continuation: Box::new(Protocol::End),
518 annotations: Annotations::new(),
519 from_annotations: Annotations::new(),
520 to_annotations: Annotations::new(),
521 };
522
523 let global = protocol_to_global(&protocol).unwrap();
524 assert!(
525 matches!(global, GlobalTypeCore::Comm { sender, receiver, .. }
526 if sender == "A" && receiver == "B")
527 );
528 }
529
530 #[test]
531 fn test_recursive_conversion() {
532 let protocol = Protocol::Rec {
534 label: ident("Loop"),
535 body: Box::new(Protocol::Send {
536 from: role("A"),
537 to: role("B"),
538 message: message("msg"),
539 continuation: Box::new(Protocol::Var(ident("Loop"))),
540 annotations: Annotations::new(),
541 from_annotations: Annotations::new(),
542 to_annotations: Annotations::new(),
543 }),
544 };
545
546 let global = protocol_to_global(&protocol).unwrap();
547 assert!(matches!(global, GlobalTypeCore::Mu { var, .. } if var == "Loop"));
548 }
549
550 #[test]
551 fn test_broadcast_unsupported() {
552 let protocol = Protocol::Broadcast {
553 from: role("A"),
554 to_all: NonEmptyVec::from_head_tail(role("B"), vec![role("C")]),
555 message: message("msg"),
556 continuation: Box::new(Protocol::End),
557 annotations: Annotations::new(),
558 from_annotations: Annotations::new(),
559 };
560
561 let result = protocol_to_global(&protocol);
562 assert!(
563 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Broadcast")
564 );
565 }
566
567 #[test]
568 fn test_loop_unsupported() {
569 let protocol = Protocol::Loop {
570 condition: None,
571 body: Box::new(Protocol::End),
572 };
573 let result = protocol_to_global(&protocol);
574 assert!(
575 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Loop")
576 );
577 }
578
579 #[test]
580 fn test_parallel_unsupported() {
581 let protocol = Protocol::Parallel {
582 protocols: NonEmptyVec::from_head_tail(Protocol::End, vec![Protocol::End]),
583 };
584 let result = protocol_to_global(&protocol);
585 assert!(
586 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Parallel")
587 );
588 }
589
590 #[test]
591 fn test_local_type_conversion() {
592 let local = LocalType::Send {
594 to: role("B"),
595 message: message("msg"),
596 continuation: Box::new(LocalType::End),
597 };
598
599 let local_r = local_to_local_r(&local).unwrap();
600 assert!(matches!(local_r, LocalTypeR::Send { partner, .. } if partner == "B"));
601 }
602
603 #[test]
604 fn test_localchoice_unsupported() {
605 let local = LocalType::LocalChoice {
606 branches: vec![(ident("L"), LocalType::End)],
607 };
608 let result = local_to_local_r(&local);
609 assert!(
610 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "LocalChoice")
611 );
612 }
613
614 #[test]
615 fn test_timeout_unsupported() {
616 let local = LocalType::Timeout {
617 duration: std::time::Duration::from_millis(100),
618 body: Box::new(LocalType::End),
619 };
620 let result = local_to_local_r(&local);
621 assert!(
622 matches!(result, Err(ConversionError::UnsupportedFeature { feature, .. }) if feature == "Timeout")
623 );
624 }
625
626 #[test]
627 fn test_message_conversion_uses_unit_payload_sort() {
628 let protocol = Protocol::Send {
629 from: role("A"),
630 to: role("B"),
631 message: MessageType {
632 name: ident("payload_msg"),
633 type_annotation: None,
634 payload: Some(quote::quote! { Vec<u8> }),
635 },
636 continuation: Box::new(Protocol::End),
637 annotations: Annotations::new(),
638 from_annotations: Annotations::new(),
639 to_annotations: Annotations::new(),
640 };
641
642 let global = protocol_to_global(&protocol).expect("conversion should succeed");
643 match global {
644 GlobalTypeCore::Comm { branches, .. } => {
645 let (label, _) = &branches[0];
646 assert_eq!(label.sort, PayloadSort::Unit);
647 }
648 _ => panic!("expected comm"),
649 }
650 }
651
652 #[test]
653 fn test_equivalence_check() {
654 let lt1 = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
655 let lt2 = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
656 let lt3 = LocalTypeR::send("C", Label::new("msg"), LocalTypeR::End);
657
658 assert!(local_types_equivalent(<1, <2));
659 assert!(!local_types_equivalent(<1, <3));
660 }
661}